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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-11 T-1: Tooltips, one behavior for every tooltip of a component (glossary terms in a
|
|
3
|
+
* paragraph, icon buttons in a toolbar): one state slice and two timers however many triggers
|
|
4
|
+
* there are, instead of a tooltip() per trigger.
|
|
5
|
+
*
|
|
6
|
+
* Article.uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }
|
|
7
|
+
*
|
|
8
|
+
* Each trigger names its tip with aria-describedby (the tip's id); the tips are `role="tooltip"
|
|
9
|
+
* popover="manual"` elements placed by CSS anchor positioning, as for tooltip(). One tip shows at
|
|
10
|
+
* a time. Moving from one trigger to another while a tip is shown switches at once (no second
|
|
11
|
+
* show delay), as tooltip groups do.
|
|
12
|
+
*
|
|
13
|
+
* Options: `trigger`, `tip` (required selectors matching every trigger / every tip),
|
|
14
|
+
* `showDelay` (500 ms), `hideDelay` (100 ms).
|
|
15
|
+
* State: { id (the tip of the trigger or tip last entered), open (the id of the tip shown),
|
|
16
|
+
* pending: null | 'show' | 'hide', hover, focus }. Actions: ENTER / LEAVE ({ via: 'hover' |
|
|
17
|
+
* 'focus', id }), SHOW / HIDE (the timers), ESCAPE, TOGGLED ({ id, open }). Timers
|
|
18
|
+
* '<key>.show' / '<key>.hide' as tooltip()'s. Needs makeTimerDriver() (tests: fake timers).
|
|
19
|
+
* Same rules as tooltip(): pointer and focus tracked apart, touch ignored, hovering the tip keeps
|
|
20
|
+
* it open (WCAG 1.4.13), Escape listened for only while a tip is shown or about to be.
|
|
21
|
+
*/
|
|
22
|
+
import {ABORT, defineBehavior, dropRepeats, xs} from '../index'
|
|
23
|
+
|
|
24
|
+
const mouse = (e: any) => e?.pointerType != 'touch'
|
|
25
|
+
const el = (e: any) => e?.ownerTarget || e?.currentTarget || e?.target
|
|
26
|
+
// the trigger's tip id (its aria-describedby: the first id when it lists several), or the tip's own
|
|
27
|
+
const tipOf = (e: any) => String(el(e)?.getAttribute?.('aria-describedby') || '').trim().split(/\s+/)[0] || null
|
|
28
|
+
const idOf = (e: any) => el(e)?.id || null
|
|
29
|
+
const at = (id: string) => `[id="${id.replace(/["\\]/g, '\\$&')}"]`
|
|
30
|
+
|
|
31
|
+
const base = /*#__PURE__*/ defineBehavior({
|
|
32
|
+
initialState: {id: null as null | string, open: null as null | string, pending: null as null | string, hover: false, focus: false},
|
|
33
|
+
timers: (s: any, o: any) => ({
|
|
34
|
+
show: s?.pending == 'show' && {after: o.showDelay ?? 500, action: 'SHOW'},
|
|
35
|
+
hide: s?.pending == 'hide' && {after: o.hideDelay ?? 100, action: 'HIDE'},
|
|
36
|
+
}),
|
|
37
|
+
intent: ({DOM, STATE}: any, {trigger, tip}: any) => ({
|
|
38
|
+
ENTER: xs.merge(
|
|
39
|
+
DOM.pointerenter(trigger).filter(mouse).map((e: any) => ({via: 'hover', id: tipOf(e)})),
|
|
40
|
+
DOM.pointerenter(tip).filter(mouse).map((e: any) => ({via: 'hover', id: idOf(e)})),
|
|
41
|
+
DOM.focus(trigger).map((e: any) => ({via: 'focus', id: tipOf(e)})),
|
|
42
|
+
).filter((x: any) => !!x.id),
|
|
43
|
+
LEAVE: xs.merge(
|
|
44
|
+
DOM.pointerleave(trigger).filter(mouse).map((e: any) => ({via: 'hover', id: tipOf(e)})),
|
|
45
|
+
DOM.pointerleave(tip).filter(mouse).map((e: any) => ({via: 'hover', id: idOf(e)})),
|
|
46
|
+
DOM.blur(trigger).map((e: any) => ({via: 'focus', id: tipOf(e)})),
|
|
47
|
+
).filter((x: any) => !!x.id),
|
|
48
|
+
ESCAPE: (STATE ? STATE.stream.map((s: any) => !!(s?.open || s?.pending)).compose(dropRepeats()) : xs.of(true))
|
|
49
|
+
.map((on: boolean) => (on ? DOM.keydown('document').key().filter((k: string) => k == 'Escape') : xs.never()))
|
|
50
|
+
.flatten(),
|
|
51
|
+
TOGGLED: DOM.toggle(tip).map((e: any) => ({id: idOf(e), open: e.newState == 'open'})).filter((x: any) => !!x.id),
|
|
52
|
+
}),
|
|
53
|
+
model: {
|
|
54
|
+
ENTER: {
|
|
55
|
+
STATE: (s: any, {via, id}: any) => {
|
|
56
|
+
const k = via == 'focus' ? 'focus' : 'hover'
|
|
57
|
+
// another trigger: it takes over (a tip already shown switches at once, in ELEMENT)
|
|
58
|
+
if (id != s.id) return {...s, id, hover: k == 'hover', focus: k == 'focus', pending: s.open ? null : 'show'}
|
|
59
|
+
const pending = s.open == id ? null : 'show'
|
|
60
|
+
return s[k] && s.pending == pending ? ABORT : {...s, [k]: true, pending}
|
|
61
|
+
},
|
|
62
|
+
// 7.0: outputs get the updated slice; "another trigger than before" reads the previous one
|
|
63
|
+
ELEMENT: (s: any, {id}: any, _n: any, p: any, _o: any, k: string) => {
|
|
64
|
+
const w = p?.previousState ? p.previousState[k] : s
|
|
65
|
+
return id != w.id && w.open && w.open != id ? [{hidePopover: at(w.open)}, {showPopover: at(id)}] : ABORT
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
LEAVE: (s: any, {via, id}: any) => {
|
|
69
|
+
if (id != s.id) return ABORT
|
|
70
|
+
const k = via == 'focus' ? 'focus' : 'hover'
|
|
71
|
+
const n = {...s, [k]: false}
|
|
72
|
+
const pending = n.hover || n.focus ? s.pending : s.open ? 'hide' : null
|
|
73
|
+
return !s[k] && s.pending == pending ? ABORT : {...n, pending}
|
|
74
|
+
},
|
|
75
|
+
SHOW: {
|
|
76
|
+
STATE: (s: any) => ({...s, pending: null}),
|
|
77
|
+
ELEMENT: (s: any) => (!s.id || s.open == s.id ? ABORT : s.open ? [{hidePopover: at(s.open)}, {showPopover: at(s.id)}] : {showPopover: at(s.id)}),
|
|
78
|
+
},
|
|
79
|
+
HIDE: {
|
|
80
|
+
STATE: (s: any) => ({...s, pending: null}),
|
|
81
|
+
ELEMENT: (s: any) => (s.open ? {hidePopover: at(s.open)} : ABORT),
|
|
82
|
+
},
|
|
83
|
+
ESCAPE: {
|
|
84
|
+
STATE: (s: any) => (s.open || s.pending ? {...s, pending: null} : ABORT),
|
|
85
|
+
ELEMENT: (s: any) => (s.open ? {hidePopover: at(s.open)} : ABORT),
|
|
86
|
+
},
|
|
87
|
+
TOGGLED: (s: any, {id, open}: any) => (open ? (s.open == id ? ABORT : {...s, open: id}) : s.open == id ? {...s, open: null} : ABORT),
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** Every tooltip of a component as one behavior: `uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }`. */
|
|
92
|
+
export const tooltips = (options: any = {}): any => base({...options, id: null, open: null, pending: null, hover: false, focus: false})
|
package/src/ui.d.ts
CHANGED
|
@@ -75,6 +75,41 @@ export interface TooltipActions { ENTER: 'hover' | 'focus' | undefined; LEAVE: '
|
|
|
75
75
|
/** A tooltip: `uses = { tip: tooltip({ trigger: '.save', tip: '.save-tip' }) }`. Needs makeTimerDriver(). */
|
|
76
76
|
export function tooltip(options: TooltipOptions): Behavior<TooltipState, TooltipActions, {}, TooltipOptions>
|
|
77
77
|
|
|
78
|
+
// ── Tooltips (PLAN-11) ────────────────────────────────────────────────
|
|
79
|
+
export interface TooltipsState {
|
|
80
|
+
/** The tip id of the trigger (or tip) last entered */
|
|
81
|
+
id: string | null
|
|
82
|
+
/** The id of the tip shown, or null */
|
|
83
|
+
open: string | null
|
|
84
|
+
pending: null | 'show' | 'hide'
|
|
85
|
+
hover: boolean
|
|
86
|
+
focus: boolean
|
|
87
|
+
}
|
|
88
|
+
export interface TooltipsOptions {
|
|
89
|
+
/** Every trigger (a selector matching them all); each names its tip with aria-describedby */
|
|
90
|
+
trigger: BehaviorTarget
|
|
91
|
+
/** Every tip: role="tooltip" popover="manual" with an id, placed with CSS anchor positioning */
|
|
92
|
+
tip: BehaviorTarget
|
|
93
|
+
/** ms before a tip shows (default 500); moving to another trigger while one shows switches at once */
|
|
94
|
+
showDelay?: number
|
|
95
|
+
/** ms before it hides (default 100) */
|
|
96
|
+
hideDelay?: number
|
|
97
|
+
}
|
|
98
|
+
export interface TooltipsActions {
|
|
99
|
+
ENTER: { via: 'hover' | 'focus'; id: string }
|
|
100
|
+
LEAVE: { via: 'hover' | 'focus'; id: string }
|
|
101
|
+
SHOW: any
|
|
102
|
+
HIDE: any
|
|
103
|
+
ESCAPE: string
|
|
104
|
+
TOGGLED: { id: string; open: boolean }
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* PLAN-11 (6.7): every tooltip of a component as one behavior (one state slice, two timers):
|
|
108
|
+
* `uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }`, each trigger with
|
|
109
|
+
* `aria-describedby` naming its tip's id. One tip shows at a time. Needs makeTimerDriver().
|
|
110
|
+
*/
|
|
111
|
+
export function tooltips(options: TooltipsOptions): Behavior<TooltipsState, TooltipsActions, {}, TooltipsOptions>
|
|
112
|
+
|
|
78
113
|
// ── Tabs ──────────────────────────────────────────────────────────────
|
|
79
114
|
export interface TabsState {
|
|
80
115
|
/** The ids' prefix (the key in `uses` unless the `id` option names it) */
|
package/src/ui.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export {dialog} from './ui/dialog'
|
|
10
10
|
export {popover} from './ui/popover'
|
|
11
11
|
export {tooltip} from './ui/tooltip'
|
|
12
|
+
export {tooltips} from './ui/tooltips'
|
|
12
13
|
export {tabs, tabsAttrs} from './ui/tabs'
|
|
13
14
|
export {accordion, accordionAttrs} from './ui/accordion'
|
|
14
15
|
export {disclosure, disclosureAttrs} from './ui/disclosure'
|
|
@@ -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
|
+
}
|