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
|
@@ -66,6 +66,21 @@ function removed(component: any, name: string, anchor: string, detail?: string):
|
|
|
66
66
|
})
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* SYG428 (6.5.0): `state="a.b"` / `from="a.b"` is a path, read as `state.a.b`. Before 6.5.0 it was
|
|
71
|
+
* one key named "a.b"; when the owner's state still has such a key, the designator now misses it.
|
|
72
|
+
*/
|
|
73
|
+
function dottedKey(f: any, what: string, designator: any, ownerState: any): void {
|
|
74
|
+
if (typeof designator != 'string' || !/[.[]/.test(designator) || !isPlainObject(ownerState)) return
|
|
75
|
+
if (!Object.prototype.hasOwnProperty.call(ownerState, designator) || !once(`SYG428:${f.name}:${designator}`)) return
|
|
76
|
+
devReport('SYG428', {
|
|
77
|
+
component: f.name,
|
|
78
|
+
message: `${what}="${designator}" is a path (since 6.5.0), read as ${designator.replace(/\[(\d+)\]/g, '.$1').split('.').map((k: string) => `[${JSON.stringify(k)}]`).join('')}, but the state has a key named "${designator}"`,
|
|
79
|
+
fix: `Rename the key so it has no dot (and point ${what} at it), or use a lens: ${what}={{ get: (s) => s['${designator}'], set: (s, v) => ({ ...s, '${designator}': v }) }}`,
|
|
80
|
+
data: {designator, what},
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
/** SYG427: CHILD.select(x) with no component (an import of a name the module doesn't export) */
|
|
70
85
|
function notComponent(f: any, x: any): void {
|
|
71
86
|
if (!once(`SYG427:${f.name}`)) return
|
|
@@ -373,11 +388,16 @@ export function nextHooks(_api: any): any {
|
|
|
373
388
|
// G-527: one chain of SYG149 checks over the latest patches (G-501: it stops once every instance
|
|
374
389
|
// that renders a named Collection is disposed, the app's dispose included)
|
|
375
390
|
const vtCheck = vtChecker(() => vtp.size && on())
|
|
391
|
+
// SYG428: the live instances by id, to read a child's parent state
|
|
392
|
+
const live = new Map<any, any>()
|
|
376
393
|
return {
|
|
377
394
|
onCreate(iv: any) {
|
|
395
|
+
live.set(iv.id, iv)
|
|
378
396
|
if (!on()) return
|
|
379
397
|
log.onCreate(iv)
|
|
380
398
|
const f = fac(iv)
|
|
399
|
+
const parent = live.get(iv.parentId)
|
|
400
|
+
if (parent) dottedKey(f, `<${f.name} state`, iv.props?.state, parent.state)
|
|
381
401
|
removedStatics(f)
|
|
382
402
|
if (!iv.def.intent) wire(iv, [])
|
|
383
403
|
},
|
|
@@ -418,6 +438,7 @@ export function nextHooks(_api: any): any {
|
|
|
418
438
|
},
|
|
419
439
|
onDispose(iv: any) {
|
|
420
440
|
vtp.delete(iv)
|
|
441
|
+
live.delete(iv.id)
|
|
421
442
|
H.onDispose(fac(iv))
|
|
422
443
|
},
|
|
423
444
|
onAction(iv: any, a: any) { log.onAction(iv, a) },
|
|
@@ -451,6 +472,7 @@ export function nextHooks(_api: any): any {
|
|
|
451
472
|
if (w.length) removed(f, f.name, 'collection-wrapper', w.map(k => k == 'dataset' ? 'data-*' : k == 'hook' ? 'ref / hook' : k).join(', '))
|
|
452
473
|
}
|
|
453
474
|
try { checkCollection(f, {data: {props}}) } catch (_) { /* ignore */ }
|
|
475
|
+
dottedKey(f, `<${sel == 'collection' ? 'Collection' : 'VirtualCollection'} from`, props.from, owner.state)
|
|
454
476
|
// G-419: SYG148 for a prefix that isn't an identifier; a valid one is checked for
|
|
455
477
|
// duplicate names after each patch (SYG149). G-460: a falsy one names nothing (as the host)
|
|
456
478
|
const p = props.viewTransitionName
|
|
@@ -96,9 +96,23 @@ export function getCodeInfo(code: string): DiagnosticCodeInfo | undefined
|
|
|
96
96
|
|
|
97
97
|
/** How an action is dispatched. */
|
|
98
98
|
/** 'agent' (static, PLAN-6 K-1): only the component's `agent` declaration dispatches it */
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* 'behavior' (static, PLAN-10 A-8): a `uses` entry's action nothing else triggers that the behavior
|
|
101
|
+
* may dispatch itself where the checker can't list it (a package behavior, an open model, ...)
|
|
102
|
+
*/
|
|
103
|
+
export type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'agent' | 'behavior' | 'unknown'
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
/**
|
|
106
|
+
* PLAN-10 A-4 (static only): a source position, 1-based. `file` (relative to the working
|
|
107
|
+
* directory) only when it isn't the component's file.
|
|
108
|
+
*/
|
|
109
|
+
export interface InspectLocation {
|
|
110
|
+
line: number
|
|
111
|
+
column: number
|
|
112
|
+
file?: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface InspectAction extends Partial<InspectLocation> {
|
|
102
116
|
name: string
|
|
103
117
|
/**
|
|
104
118
|
* 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
|
|
@@ -112,18 +126,56 @@ export interface InspectAction {
|
|
|
112
126
|
trigger: InspectActionTrigger
|
|
113
127
|
/** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
|
|
114
128
|
sinks: string[]
|
|
129
|
+
/** the `uses` key of the behavior that owns it ('pager' for 'pager.NEXT') */
|
|
130
|
+
behavior?: string
|
|
131
|
+
/** static, PLAN-10 A-4: the model entry (line/column on the action: the intent property, else this) */
|
|
132
|
+
modelAt?: InspectLocation
|
|
133
|
+
/**
|
|
134
|
+
* static, PLAN-10 A-7 (actions with a STATE sink only): the top-level state keys the STATE
|
|
135
|
+
* reducer(s) may write (a behavior's: its slice); [] when it never changes state; null when unknown
|
|
136
|
+
*/
|
|
137
|
+
writes?: string[] | null
|
|
138
|
+
/** static, PLAN-10 A-7: a STATE reducer may return undefined (removes a Collection item) */
|
|
139
|
+
removes?: true
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
export interface InspectChild {
|
|
142
|
+
export interface InspectChild extends Partial<InspectLocation> {
|
|
118
143
|
name: string
|
|
119
|
-
|
|
144
|
+
/**
|
|
145
|
+
* 'layout' (static, PLAN-10 A-9): Vike's shell, a +Wrapper / +Layout rendering the next +Layout in
|
|
146
|
+
* or a +Page it applies to (Wrapper(s) > Layout(s) > Page); no location (no JSX site)
|
|
147
|
+
*/
|
|
148
|
+
via: 'tag' | 'collection' | 'switchable' | 'slot' | 'layout'
|
|
120
149
|
/** Collection: the `from` state field, when known */
|
|
121
150
|
from?: string | null
|
|
122
151
|
/** runtime: number of live instances behind this entry (e.g. Collection items) */
|
|
123
152
|
count?: number
|
|
153
|
+
/** runtime, PLAN-10 D522: the ids of those instances */
|
|
154
|
+
ids?: string[]
|
|
155
|
+
/**
|
|
156
|
+
* static, PLAN-10 A-3: the id of the component it renders (resolved through the import); null when
|
|
157
|
+
* it isn't in the graph (a package, a widget, a file not scanned, a dynamic value)
|
|
158
|
+
*/
|
|
159
|
+
id?: string | null
|
|
160
|
+
/** static, PLAN-10 A-1: a literal `state` lens (`<Greeting state="host" />`) */
|
|
161
|
+
state?: string
|
|
162
|
+
/** static, PLAN-10 A-2: the package the tag is imported from ('sygnal/ui') */
|
|
163
|
+
external?: string
|
|
164
|
+
/** static, PLAN-10 A-2: a widget tag (defineWidget, fromZag, fromReact, the sygnal/ui Zag parts) */
|
|
165
|
+
widget?: true
|
|
124
166
|
}
|
|
125
167
|
|
|
126
|
-
|
|
168
|
+
/** PLAN-10 A-5 (static): a CHILD.select(Child) call in an intent */
|
|
169
|
+
export interface InspectChildSelect extends InspectLocation {
|
|
170
|
+
/** the selected component's name as written */
|
|
171
|
+
child: string | null
|
|
172
|
+
/** its id; null when it isn't in the graph */
|
|
173
|
+
id: string | null
|
|
174
|
+
/** the intent actions whose stream contains the call (the receivers of the child's PARENT sink) */
|
|
175
|
+
actions: string[]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface InspectSelector extends Partial<InspectLocation> {
|
|
127
179
|
/** CSS selector passed to DOM.select() (chained selects joined with a space) */
|
|
128
180
|
selector: string
|
|
129
181
|
/** event types listened to; null when unknown (runtime, real DOM) */
|
|
@@ -139,10 +191,12 @@ export interface InspectSelector {
|
|
|
139
191
|
isolationHit: string | null
|
|
140
192
|
/** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
|
|
141
193
|
control?: string
|
|
194
|
+
/** the `uses` key of the behavior that listens to it */
|
|
195
|
+
behavior?: string
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
/** PLAN-4 CT-1: a control (controls()) a component renders */
|
|
145
|
-
export interface InspectControl {
|
|
199
|
+
export interface InspectControl extends Partial<InspectLocation> {
|
|
146
200
|
/** the control's key (rendered as data-control="<name>") */
|
|
147
201
|
name: string
|
|
148
202
|
/** the intrinsic tag of a tag spec; null for a spec object */
|
|
@@ -157,10 +211,14 @@ export interface InspectDiagnostic {
|
|
|
157
211
|
code: string
|
|
158
212
|
severity: DiagnosticSeverity
|
|
159
213
|
component?: string
|
|
214
|
+
/** static, PLAN-10 A-3: the id of the component it is attached to */
|
|
215
|
+
componentId?: string
|
|
160
216
|
message: string
|
|
161
217
|
fix?: string
|
|
162
218
|
docsUrl?: string
|
|
163
219
|
data?: any
|
|
220
|
+
/** runtime only: when it was reported (ms since the epoch; each is reported once per process) */
|
|
221
|
+
timestamp?: number
|
|
164
222
|
/** static only */
|
|
165
223
|
file?: string
|
|
166
224
|
line?: number
|
|
@@ -215,6 +273,12 @@ export interface InspectComponent {
|
|
|
215
273
|
name: string
|
|
216
274
|
/** runtime: instance number (as a string); static: 'file:line' of the definition */
|
|
217
275
|
id: string
|
|
276
|
+
/**
|
|
277
|
+
* static, PLAN-10 D521: 'file#name', stable when lines move (hosts key selection and positions by it);
|
|
278
|
+
* a second component of the same name in one file gets 'file#name#2', and so on. Runtime (PLAN-10 P-2,
|
|
279
|
+
* D544): the same key when sygnal/vite's dev server stamped the component function, else absent
|
|
280
|
+
*/
|
|
281
|
+
key?: string
|
|
218
282
|
/** runtime: the parent instance's id; static: null (see the parents' `children`) */
|
|
219
283
|
parentId: string | null
|
|
220
284
|
/** static only: source file, relative to the working directory */
|
|
@@ -224,6 +288,32 @@ export interface InspectComponent {
|
|
|
224
288
|
* component renders it, else how it is first rendered
|
|
225
289
|
*/
|
|
226
290
|
kind: 'root' | 'child' | 'collection-item' | 'switchable'
|
|
291
|
+
/** PLAN-10 A-1 / D522: rendered as a component (or started as an entry) but has no statics */
|
|
292
|
+
viewOnly?: true
|
|
293
|
+
/**
|
|
294
|
+
* static, PLAN-10 A-6: started by run(), defineElement() or a Vike +Page / +Layout / +Wrapper; 'unknown' on the
|
|
295
|
+
* other roots when an entry call's component can't be resolved (picked at run time).
|
|
296
|
+
* runtime (D522): true on an app's root instance
|
|
297
|
+
*/
|
|
298
|
+
entry?: true | 'unknown'
|
|
299
|
+
/**
|
|
300
|
+
* static, Vike only (PLAN-10 A-9): on a +Page's component its route ('/orders/@id': from the
|
|
301
|
+
* filesystem, or the literal string a +route file / the config's `route` sets); null for a Route
|
|
302
|
+
* Function or a route the checker can't read (with `routeDynamic`) and for _error/+Page. On a
|
|
303
|
+
* +Layout's / +Wrapper's: the URL prefix of the directory it applies to
|
|
304
|
+
*/
|
|
305
|
+
route?: string | null
|
|
306
|
+
/**
|
|
307
|
+
* static, Vike only (PLAN-10 A-9): every route of a component several +Page files export (`route` is
|
|
308
|
+
* the first, in path order; null for a dynamic one or the error page); omitted otherwise
|
|
309
|
+
*/
|
|
310
|
+
routes?: (string | null)[]
|
|
311
|
+
/** static, Vike only (PLAN-10 A-9): the route is a Route Function or can't be read (route: null) */
|
|
312
|
+
routeDynamic?: true
|
|
313
|
+
/** runtime, PLAN-10 D522: the app (run()) it belongs to: its root's index among the live roots, oldest first */
|
|
314
|
+
app?: number
|
|
315
|
+
/** runtime, PLAN-10 D522: false on a hidden Switchable page and below it; omitted when shown */
|
|
316
|
+
shown?: false
|
|
227
317
|
actions: InspectAction[]
|
|
228
318
|
/** state keys (runtime: current state; static: initialState) */
|
|
229
319
|
stateKeys: string[]
|
|
@@ -231,14 +321,27 @@ export interface InspectComponent {
|
|
|
231
321
|
calculated: string[]
|
|
232
322
|
/** context fields this component provides (Component.context) */
|
|
233
323
|
contextProvides: string[]
|
|
234
|
-
/** context fields its view reads (static
|
|
324
|
+
/** context fields its view reads (static: the view source; runtime, D530: what its latest render read) */
|
|
235
325
|
contextConsumes?: string[] | null
|
|
326
|
+
/**
|
|
327
|
+
* static, PLAN-10 A-7: top-level state keys the view (and its child lenses, calculated and
|
|
328
|
+
* context functions) reads literally; omitted when none
|
|
329
|
+
*/
|
|
330
|
+
stateReads?: string[]
|
|
331
|
+
/** static, PLAN-10 A-7: stateReads may be incomplete (the state used whole somewhere) */
|
|
332
|
+
stateReadsPartial?: true
|
|
236
333
|
/** EVENTS types this component emits */
|
|
237
334
|
eventsEmitted: string[]
|
|
238
335
|
/** EVENTS types this component selects ('*' = EVENTS.select() without a type) */
|
|
239
336
|
eventsSelected: string[]
|
|
240
337
|
children: InspectChild[]
|
|
241
338
|
selectors: InspectSelector[]
|
|
339
|
+
/** static, PLAN-10 A-4: where the `context` static's entries are */
|
|
340
|
+
contextEntries?: Array<InspectLocation & { name: string }>
|
|
341
|
+
/** static, PLAN-10 A-4: the EVENTS emit and select sites of its intent and model */
|
|
342
|
+
eventSites?: Array<InspectLocation & { type: string; role: 'emit' | 'select' }>
|
|
343
|
+
/** static, PLAN-10 A-5: CHILD.select(Child) in its intent */
|
|
344
|
+
childSelects?: InspectChildSelect[]
|
|
242
345
|
/** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
|
|
243
346
|
controls?: InspectControl[]
|
|
244
347
|
/** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
|
|
@@ -253,7 +356,7 @@ export interface InspectComponent {
|
|
|
253
356
|
}
|
|
254
357
|
|
|
255
358
|
/** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
|
|
256
|
-
export interface InspectCommand {
|
|
359
|
+
export interface InspectCommand extends Partial<InspectLocation> {
|
|
257
360
|
/** the model entry that sends it */
|
|
258
361
|
action: string
|
|
259
362
|
/** the command's method (its first key) */
|
|
@@ -275,13 +378,15 @@ export interface InspectAgent {
|
|
|
275
378
|
description: string | null
|
|
276
379
|
/** the declaration has a read projection */
|
|
277
380
|
read: boolean
|
|
278
|
-
actions: Array<{ name: string; consequential: boolean; input: boolean }
|
|
381
|
+
actions: Array<{ name: string; consequential: boolean; input: boolean } & Partial<InspectLocation>>
|
|
382
|
+
/** the read-only `agent.queries` tools; omitted when none */
|
|
383
|
+
queries?: Array<{ name: string; input: boolean } & Partial<InspectLocation>>
|
|
279
384
|
/** spreads: what is listed may be incomplete */
|
|
280
385
|
partial?: true
|
|
281
386
|
}
|
|
282
387
|
|
|
283
388
|
/** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
|
|
284
|
-
export interface InspectTimer {
|
|
389
|
+
export interface InspectTimer extends Partial<InspectLocation> {
|
|
285
390
|
name: string
|
|
286
391
|
every?: number | null
|
|
287
392
|
after?: number | null
|
|
@@ -316,8 +421,12 @@ export interface InspectGraph {
|
|
|
316
421
|
/** which implementation produced the graph */
|
|
317
422
|
source?: 'runtime' | 'static'
|
|
318
423
|
components: InspectComponent[]
|
|
319
|
-
/**
|
|
320
|
-
|
|
424
|
+
/**
|
|
425
|
+
* EVENTS bus: type -> names of the components that emit / select it (each name once); emitterIds /
|
|
426
|
+
* selectorIds (PLAN-10 A-3, D522, D528): the id of every such component (static) or instance
|
|
427
|
+
* (runtime), not parallel to the names
|
|
428
|
+
*/
|
|
429
|
+
events: Record<string, { emitters: string[]; selectors: string[]; emitterIds?: string[]; selectorIds?: string[] }>
|
|
321
430
|
/** diagnostics not tied to a listed component */
|
|
322
431
|
diagnostics: InspectDiagnostic[]
|
|
323
432
|
/** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
|
|
@@ -152,6 +152,9 @@ export const CODE_TITLES: Record<string, string> = {
|
|
|
152
152
|
SYG127: 'Behavior collision or unresolvable uses entry',
|
|
153
153
|
SYG128: 'Duplicate control key',
|
|
154
154
|
SYG129: "CHILD.select() of a component this one doesn't render",
|
|
155
|
+
// PLAN-10 X-1 (D512): graph-wide checks, static only (sygnal-check)
|
|
156
|
+
SYG170: 'Context field read with no provider',
|
|
157
|
+
SYG171: 'PARENT sink no parent reads',
|
|
155
158
|
SYG112: 'Reply action has no model entry',
|
|
156
159
|
SYG130: 'href() names no route or leaves out a param',
|
|
157
160
|
SYG131: 'Route params the pattern does not use',
|
|
@@ -281,6 +284,8 @@ export const CODE_TITLES: Record<string, string> = {
|
|
|
281
284
|
// checks/next.ts and sygnal-check: a component reference that is not a function (an import of
|
|
282
285
|
// a name the module doesn't export is undefined under Vitest)
|
|
283
286
|
SYG427: 'CHILD.select() of something that is not a component',
|
|
287
|
+
// 6.5.0 (checks/next.ts): state="a.b" / from="a.b" is a path; the parent state has a literal "a.b" key
|
|
288
|
+
SYG428: 'A state path that names a key containing a dot',
|
|
284
289
|
// PLAN-9 (D470): checks/next.ts; dev entry
|
|
285
290
|
SYG453: 'Two children with the same identity share one instance',
|
|
286
291
|
SYG601: "Retired in 6.0: Missing or invalid sources",
|
|
@@ -425,6 +430,9 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
|
|
|
425
430
|
SYG128: 'error',
|
|
426
431
|
// PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
|
|
427
432
|
SYG129: 'warn',
|
|
433
|
+
// PLAN-10 X-1: static only (sygnal-check)
|
|
434
|
+
SYG170: 'warn',
|
|
435
|
+
SYG171: 'warn',
|
|
428
436
|
SYG221: 'error',
|
|
429
437
|
// PLAN-4 GS-4 (state.ts)
|
|
430
438
|
SYG222: 'warn',
|
|
@@ -491,6 +499,8 @@ export const DEV_CODE_SEVERITY: Record<string, DiagnosticSeverity> = {
|
|
|
491
499
|
SYG426: 'warn',
|
|
492
500
|
// checks/next.ts: CHILD.select(undefined), usually a missing export
|
|
493
501
|
SYG427: 'error',
|
|
502
|
+
// checks/next.ts: a path designator that 6.4 read as one key with a dot
|
|
503
|
+
SYG428: 'warn',
|
|
494
504
|
// PLAN-9 (checks/next.ts): D470 key identity, two children with one identity
|
|
495
505
|
SYG453: 'warn',
|
|
496
506
|
SYG612: 'error',
|
package/src/index.d.ts
CHANGED
|
@@ -59,7 +59,9 @@ export type ViewProps<STATE = any, PROPS = {}, CONTEXT = {}> =
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* The `state` prop a parent passes to a sub-component in JSX: the name of a field of the
|
|
62
|
-
* parent's state (`state="editor"`)
|
|
62
|
+
* parent's state (`state="editor"`), a path to nested state (`state="settings.rules"`,
|
|
63
|
+
* `state="boards[0].title"`; since 6.5.0: a write copies only the levels on the path), or a
|
|
64
|
+
* lens. Without it the child shares the parent's state.
|
|
63
65
|
*/
|
|
64
66
|
export type StateProp = string | Lense<any, any>
|
|
65
67
|
|
|
@@ -1574,13 +1576,25 @@ export type ArrayKeysOf<STATE> = {
|
|
|
1574
1576
|
[KEY in keyof STATE]-?: NonNullable<STATE[KEY]> extends ReadonlyArray<any> ? KEY : never
|
|
1575
1577
|
}[keyof STATE] & string
|
|
1576
1578
|
|
|
1579
|
+
/**
|
|
1580
|
+
* Array-valued keys of STATE and dotted paths to arrays in its nested objects, up to three levels
|
|
1581
|
+
* (`'tasks' | 'board.cards'`; since 6.5.0). Paths through array positions (`'boards.0.cards'`)
|
|
1582
|
+
* aren't listed: for a list item, use `byId`.
|
|
1583
|
+
*/
|
|
1584
|
+
export type ArrayPathsOf<STATE, DEPTH extends unknown[] = []> = DEPTH['length'] extends 3 ? never : {
|
|
1585
|
+
[KEY in keyof STATE & string]-?: NonNullable<STATE[KEY]> extends ReadonlyArray<any> ? KEY
|
|
1586
|
+
: NonNullable<STATE[KEY]> extends Record<string, any> ? `${KEY}.${ArrayPathsOf<NonNullable<STATE[KEY]>, [...DEPTH, 1]>}`
|
|
1587
|
+
: never
|
|
1588
|
+
}[keyof STATE & string]
|
|
1589
|
+
|
|
1577
1590
|
/**
|
|
1578
1591
|
* Valid `from` values for a Collection: any string or Lense while the parent STATE is unknown
|
|
1579
|
-
* (`any`); otherwise an array-valued key of STATE
|
|
1592
|
+
* (`any`); otherwise an array-valued key of STATE, a dotted path to one (`'board.cards'`), or a
|
|
1593
|
+
* Lense over STATE.
|
|
1580
1594
|
*/
|
|
1581
1595
|
export type CollectionFrom<STATE = any> = 0 extends (1 & STATE)
|
|
1582
1596
|
? string | Lense
|
|
1583
|
-
:
|
|
1597
|
+
: ArrayPathsOf<STATE> | Lense<STATE, any>
|
|
1584
1598
|
|
|
1585
1599
|
/**
|
|
1586
1600
|
* Collection props. Pass the parent component's state type as STATE to type-check `from`:
|
|
@@ -1797,6 +1811,8 @@ export type {
|
|
|
1797
1811
|
InspectRecentAction,
|
|
1798
1812
|
InspectCommand,
|
|
1799
1813
|
InspectTimer,
|
|
1814
|
+
InspectLocation,
|
|
1815
|
+
InspectChildSelect,
|
|
1800
1816
|
} from './extra/diagnostics/checks/public'
|
|
1801
1817
|
|
|
1802
1818
|
/**
|
|
@@ -13,12 +13,16 @@
|
|
|
13
13
|
* - Wrappers are outermost (context providers, state management)
|
|
14
14
|
* - Layouts are inside wrappers (visual structure, navigation chrome)
|
|
15
15
|
* - Page is innermost (route-specific content, swapped on navigation)
|
|
16
|
+
* Cumulative Layouts / Wrappers nest the root's outermost (G-803, see shell.ts). Navigating to a
|
|
17
|
+
* page with another shell (pages/admin/+Layout) keeps the shell components both pages share
|
|
18
|
+
* mounted, from the outside in, and adds or removes the rest.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
// Import from the package entry so rollup externalizes it
|
|
19
22
|
// @ts-ignore — resolved at runtime via package exports
|
|
20
23
|
import { run } from 'sygnal'
|
|
21
24
|
import { owned } from '../extra/owned'
|
|
25
|
+
import { shellOf, uidPart, type ShellPart } from './shell'
|
|
22
26
|
|
|
23
27
|
declare global {
|
|
24
28
|
interface Window {
|
|
@@ -47,6 +51,8 @@ interface PageContext {
|
|
|
47
51
|
|
|
48
52
|
/** Track the running Sygnal app for disposal on navigation */
|
|
49
53
|
let currentApp: { sources: any; sinks: any; dispose: () => void } | null = null
|
|
54
|
+
/** G-803: the running shell, outermost first (read by the wrapper's view); null: an app without one */
|
|
55
|
+
let currentShell: ShellPart[] | null = null
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
58
|
* Mutable references read by the wrapper's view function.
|
|
@@ -59,16 +65,6 @@ let currentRouteParams: any = {}
|
|
|
59
65
|
let currentUrlPathname: string = ''
|
|
60
66
|
let pageNavCounter: number = 0
|
|
61
67
|
|
|
62
|
-
/**
|
|
63
|
-
* PLAN-4 G-207: the uid() path part of a shell component or the Page: wrapper_0 → 'w0',
|
|
64
|
-
* layout_1 → 'l1', page → 'p'. Given as the nested vnode's `id` prop, so the Page's uid is
|
|
65
|
-
* 'u-w0-l0-p' however the shell nests it; onRenderHtml renders each one with the same root
|
|
66
|
-
* (renderToString's `uid`), so server and client ids match. (Same helper in onRenderHtml.ts.)
|
|
67
|
-
*/
|
|
68
|
-
function uidPart(key: string): string {
|
|
69
|
-
return key == 'page' ? 'p' : key[0] + key.slice(key.indexOf('_') + 1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
68
|
/**
|
|
73
69
|
* PLAN-4.6 R4 (04 §3.11): the core instantiates a component from the vnode's `data.c`, a
|
|
74
70
|
* component function, and reads its statics there (no options object). A shell component or a
|
|
@@ -143,11 +139,11 @@ function pageChildVNode(pageState: any, stateField: any = 'page'): any {
|
|
|
143
139
|
*
|
|
144
140
|
* Nesting order: Wrapper(s) > Layout(s) > Page
|
|
145
141
|
*
|
|
146
|
-
* The wrapper's view reads `currentPage` from the mutable closure, so
|
|
147
|
-
* on client-side navigation only the Page sub-component
|
|
148
|
-
* the Layout and Wrapper stay mounted with their state preserved.
|
|
142
|
+
* The wrapper's view reads `currentPage` and `currentShell` from the mutable closure, so
|
|
143
|
+
* on client-side navigation only the Page sub-component (and the shell components the new page
|
|
144
|
+
* doesn't share) change while the Layout and Wrapper stay mounted with their state preserved.
|
|
149
145
|
*/
|
|
150
|
-
function createLayoutWrapper(
|
|
146
|
+
function createLayoutWrapper(Page: any): any {
|
|
151
147
|
currentPage = Page
|
|
152
148
|
currentPageName = Page.componentName || Page.name || 'VikePageComponent'
|
|
153
149
|
|
|
@@ -158,9 +154,6 @@ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
|
|
|
158
154
|
// one (and the Page) gets a { get, set } lens onto its own ROOT slice: an outer component's
|
|
159
155
|
// reducer replacing its state can't wipe the inner slices (R2-2), and no view sees another
|
|
160
156
|
// component's slice.
|
|
161
|
-
const keys = shellKeys(wrappers, layouts)
|
|
162
|
-
const shell = [...wrappers, ...layouts].map((comp: any, i: number) => ({ comp, key: keys[i] }))
|
|
163
|
-
|
|
164
157
|
// The latest root state. Only the outermost lens gets the root itself; the nested lenses get
|
|
165
158
|
// their parent shell component's state. In both directions the outermost lens runs first and
|
|
166
159
|
// synchronously (state: root -> outer get -> inner get; a reducer: outer get, inner get, inner
|
|
@@ -180,10 +173,13 @@ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
|
|
|
180
173
|
return outermost ? root : { ...state }
|
|
181
174
|
},
|
|
182
175
|
})
|
|
183
|
-
|
|
176
|
+
// one lens per key and place (outermost or not), the same object on every render
|
|
177
|
+
const lenses: Record<string, any> = {}
|
|
178
|
+
const lensOf = (key: string, outermost: boolean) => (lenses[key + outermost] ||= rootLens(key, outermost, true))
|
|
184
179
|
const pageLens = rootLens('page', false, false)
|
|
185
180
|
|
|
186
181
|
function LayoutWrapperView({ state }: any) {
|
|
182
|
+
const shell = currentShell || []
|
|
187
183
|
if (shell.length === 0) {
|
|
188
184
|
// No wrappers or layouts — render Page directly
|
|
189
185
|
return pageChildVNode(state.page)
|
|
@@ -193,14 +189,14 @@ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
|
|
|
193
189
|
let inner: any = componentVNode(
|
|
194
190
|
shell[lastIdx].comp,
|
|
195
191
|
shell[lastIdx].key,
|
|
196
|
-
|
|
192
|
+
lensOf(shell[lastIdx].key, lastIdx === 0),
|
|
197
193
|
[pageChildVNode(state.page, pageLens)],
|
|
198
194
|
state[shell[lastIdx].key]
|
|
199
195
|
)
|
|
200
196
|
|
|
201
197
|
// Wrap with outer shell components
|
|
202
198
|
for (let i = lastIdx - 1; i >= 0; i--) {
|
|
203
|
-
inner = componentVNode(shell[i].comp, shell[i].key,
|
|
199
|
+
inner = componentVNode(shell[i].comp, shell[i].key, lensOf(shell[i].key, i === 0), [inner], state[shell[i].key])
|
|
204
200
|
}
|
|
205
201
|
|
|
206
202
|
// Wrap in a plain div so the view returns a regular DOM vnode.
|
|
@@ -223,6 +219,7 @@ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
|
|
|
223
219
|
// The wrapper's initial state: a sibling slice for each shell component and the Page.
|
|
224
220
|
// Must match onRenderHtml's serialized state.
|
|
225
221
|
const initialState: any = {}
|
|
222
|
+
const shell = currentShell || []
|
|
226
223
|
shell.forEach(({ comp, key }: any) => { initialState[key] = { ...(comp.initialState || {}) } })
|
|
227
224
|
initialState.page = Page.initialState || {}
|
|
228
225
|
// G-275: owned (built from the hydrated state and pageContext.data: never frozen in dev)
|
|
@@ -250,33 +247,18 @@ function createLayoutWrapper(wrappers: any[], layouts: any[], Page: any): any {
|
|
|
250
247
|
return LayoutWrapperView
|
|
251
248
|
}
|
|
252
249
|
|
|
253
|
-
/** State keys of the shell components, outermost first: wrapper_0, ..., layout_0, ... */
|
|
254
|
-
function shellKeys(wrappers: any[], layouts: any[]): string[] {
|
|
255
|
-
return [
|
|
256
|
-
...wrappers.map((_: any, i: number) => 'wrapper_' + i),
|
|
257
|
-
...layouts.map((_: any, i: number) => 'layout_' + i),
|
|
258
|
-
]
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Normalize a cumulative config value into an array of functions.
|
|
263
|
-
*/
|
|
264
|
-
function toComponentArray(val: any): any[] {
|
|
265
|
-
if (!val) return []
|
|
266
|
-
return (Array.isArray(val) ? val : [val]).filter((c: any) => typeof c === 'function')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
250
|
export function onRenderClient(pageContext: PageContext) {
|
|
270
251
|
const { Page, config } = pageContext
|
|
271
252
|
const data = pageContext.data || {}
|
|
272
253
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
254
|
+
// The shell, outermost first (G-803: Vike lists cumulative configs closest first); the state
|
|
255
|
+
// slices of its components and the Page's are siblings at the root (D50)
|
|
256
|
+
const parts = shellOf(config)
|
|
257
|
+
const hasShell = parts.length > 0
|
|
258
|
+
const shell = parts.map(p => p.key)
|
|
276
259
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
const shell = shellKeys(wrappers, layouts)
|
|
260
|
+
// a shell page after a page without one (or the reverse, below): a new app
|
|
261
|
+
if (hasShell && currentApp && !currentShell) { currentApp.dispose(); currentApp = null }
|
|
280
262
|
|
|
281
263
|
// PLAN-3 5-5 (H-7): seed the caches of the fetch drivers (makeFetchDriver({ cache: queryCache() })
|
|
282
264
|
// exposes it as driver.cache) before the page renders, so its resources start at 'success'
|
|
@@ -304,8 +286,22 @@ export function onRenderClient(pageContext: PageContext) {
|
|
|
304
286
|
pageNavCounter++
|
|
305
287
|
|
|
306
288
|
const newPageState = { ...(Page.initialState || {}), ...data }
|
|
307
|
-
// replace the page slice, keeping every shell slice (G-106, D50)
|
|
308
|
-
|
|
289
|
+
// replace the page slice, keeping every shell slice (G-106, D50). G-803: a page with another
|
|
290
|
+
// shell keeps the slices of the components it shares at the same place (key) and starts the
|
|
291
|
+
// others from their initialState
|
|
292
|
+
const old = currentShell!
|
|
293
|
+
const same = parts.length === old.length && parts.every((p, i) => p.comp === old[i].comp)
|
|
294
|
+
let swapPage = (state: any) => ({ ...state, page: newPageState })
|
|
295
|
+
if (!same) {
|
|
296
|
+
currentShell = parts
|
|
297
|
+
swapPage = (state: any) => {
|
|
298
|
+
const next: any = { page: newPageState }
|
|
299
|
+
for (const { comp, key } of parts) {
|
|
300
|
+
next[key] = old.some(o => o.key === key && o.comp === comp) ? state[key] : { ...(comp.initialState || {}) }
|
|
301
|
+
}
|
|
302
|
+
return next
|
|
303
|
+
}
|
|
304
|
+
}
|
|
309
305
|
// (PLAN-4.6 core: through the runtime API, 04 §3.11)
|
|
310
306
|
const rt = (currentApp as any).__runtime
|
|
311
307
|
rt?.setState('root', swapPage)
|
|
@@ -322,7 +318,7 @@ export function onRenderClient(pageContext: PageContext) {
|
|
|
322
318
|
if (initialState && initialState[shell[shell.length - 1]] !== undefined) {
|
|
323
319
|
// Hydrated shell state: sibling slices (D50), or the 5.4.0 shape with the Page state
|
|
324
320
|
// under the innermost shell slice (R2-9) — distribute the slices to each component
|
|
325
|
-
const allShellComps =
|
|
321
|
+
const allShellComps = parts.map(p => p.comp)
|
|
326
322
|
let pageState = initialState.page
|
|
327
323
|
shell.forEach((key: string, i: number) => {
|
|
328
324
|
let slice = initialState[key]
|
|
@@ -348,7 +344,8 @@ export function onRenderClient(pageContext: PageContext) {
|
|
|
348
344
|
urlPathname: () => currentUrlPathname,
|
|
349
345
|
}
|
|
350
346
|
|
|
351
|
-
|
|
347
|
+
currentShell = parts
|
|
348
|
+
const Component = createLayoutWrapper(Page)
|
|
352
349
|
|
|
353
350
|
try {
|
|
354
351
|
// 3-B2: `__vike` marks the shell's components (Page, Layouts, Wrappers: sub-components of
|
|
@@ -372,6 +369,7 @@ export function onRenderClient(pageContext: PageContext) {
|
|
|
372
369
|
currentApp.dispose()
|
|
373
370
|
currentApp = null
|
|
374
371
|
}
|
|
372
|
+
currentShell = null
|
|
375
373
|
|
|
376
374
|
let initialState: any
|
|
377
375
|
// 3-B2: the server's state is here when the page hydrates its HTML
|
package/src/vike/onRenderHtml.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
// @ts-ignore — resolved at runtime via package exports
|
|
18
18
|
import { renderToString, renderHead, queryCache } from 'sygnal'
|
|
19
|
+
import { shellOf, uidPart } from './shell'
|
|
19
20
|
|
|
20
21
|
interface PageContext {
|
|
21
22
|
Page: any
|
|
@@ -54,16 +55,6 @@ function esc(str: string): string {
|
|
|
54
55
|
.replace(/"/g, '"')
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* PLAN-4 G-207: the uid() path part of a shell component or the Page (wrapper_0 → 'w0',
|
|
59
|
-
* layout_1 → 'l1', page → 'p'), as onRenderClient gives each nested vnode in its `id` prop. Each
|
|
60
|
-
* one renders here as its own root, so its root is 'u' plus the parts of the shell around it and
|
|
61
|
-
* its own: the same uids as the client's nested instances.
|
|
62
|
-
*/
|
|
63
|
-
function uidPart(key: string): string {
|
|
64
|
-
return key == 'page' ? 'p' : key[0] + key.slice(key.indexOf('_') + 1)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
export function onRenderHtml(pageContext: PageContext) {
|
|
68
59
|
const { Page, config } = pageContext
|
|
69
60
|
const data = pageContext.data || {}
|
|
@@ -128,22 +119,10 @@ export function onRenderHtml(pageContext: PageContext) {
|
|
|
128
119
|
// PLAN-3 5-5 (H-7): the seeded cache the components' resources render from
|
|
129
120
|
const cache = pageContext.queryCache ? queryCache({ initial: pageContext.queryCache }) : undefined
|
|
130
121
|
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
: []
|
|
136
|
-
const layoutArray = config.Layout
|
|
137
|
-
? (Array.isArray(config.Layout) ? config.Layout : [config.Layout])
|
|
138
|
-
.filter((L: any) => typeof L === 'function')
|
|
139
|
-
: []
|
|
140
|
-
const hasShell = wrapperArray.length > 0 || layoutArray.length > 0
|
|
141
|
-
// Combined shell: wrappers (outermost) + layouts (innermost around Page)
|
|
142
|
-
// Must match the order used in onRenderClient's createLayoutWrapper.
|
|
143
|
-
const shell = [
|
|
144
|
-
...wrapperArray.map((w: any, i: number) => ({ comp: w, key: 'wrapper_' + i })),
|
|
145
|
-
...layoutArray.map((l: any, i: number) => ({ comp: l, key: 'layout_' + i })),
|
|
146
|
-
]
|
|
122
|
+
// Wrapper(s) and/or Layout(s), outermost first (G-803: Vike lists them closest first; shellOf
|
|
123
|
+
// reverses them, the same order as onRenderClient's): they decide the hydration state's shape
|
|
124
|
+
const shell = shellOf(config)
|
|
125
|
+
const hasShell = shell.length > 0
|
|
147
126
|
// G-207: with a shell, the client nests each shell component and the Page in the one outside it
|
|
148
127
|
// with an `id` prop (onRenderClient), so their uid roots are 'u-w0', 'u-w0-l0', 'u-w0-l0-p'
|
|
149
128
|
const uidRoot = (i: number) => ['u', ...shell.slice(0, i + 1).map(s => uidPart(s.key))].join('-')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-803: the shell components of a page (its Wrappers and Layouts), outermost first, with their
|
|
3
|
+
* state keys. Shared by onRenderHtml and onRenderClient so the server and the client nest them
|
|
4
|
+
* the same way.
|
|
5
|
+
*
|
|
6
|
+
* Vike's cumulative configs (`Layout` and `Wrapper` are, see +config.ts) list the values closest
|
|
7
|
+
* to the page first: pages/admin/+Layout before pages/+Layout (vike's resolveVikeConfigInternal:
|
|
8
|
+
* sortAfterInheritanceOrder puts the longer inheritance root first). So the root's come last, and
|
|
9
|
+
* the shell reverses each list: every Wrapper outside every Layout (as vike-react), and within each
|
|
10
|
+
* the root's outermost. Keys count from the outside in (wrapper_0, ..., layout_0, ...), so a root
|
|
11
|
+
* Layout keeps `layout_0` on every page, nested or not.
|
|
12
|
+
*/
|
|
13
|
+
export interface ShellPart { comp: any; key: string }
|
|
14
|
+
|
|
15
|
+
const comps = (val: any): any[] =>
|
|
16
|
+
(val ? (Array.isArray(val) ? val : [val]) : []).filter((c: any) => typeof c === 'function')
|
|
17
|
+
|
|
18
|
+
export function shellOf(config: { Wrapper?: any; Layout?: any }): ShellPart[] {
|
|
19
|
+
return [
|
|
20
|
+
...comps(config.Wrapper).reverse().map((comp, i) => ({ comp, key: 'wrapper_' + i })),
|
|
21
|
+
...comps(config.Layout).reverse().map((comp, i) => ({ comp, key: 'layout_' + i })),
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* PLAN-4 G-207: the uid() path part of a shell component or the Page: wrapper_0 → 'w0',
|
|
27
|
+
* layout_1 → 'l1', page → 'p'. onRenderClient gives it as the nested vnode's `id` prop, so the
|
|
28
|
+
* Page's uid is 'u-w0-l0-p' however the shell nests it; onRenderHtml renders each one with the
|
|
29
|
+
* same root (renderToString's `uid`), so server and client ids match.
|
|
30
|
+
*/
|
|
31
|
+
export function uidPart(key: string): string {
|
|
32
|
+
return key == 'page' ? 'p' : key[0] + key.slice(key.indexOf('_') + 1)
|
|
33
|
+
}
|