foldkit 0.145.0 → 0.147.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.
Files changed (57) hide show
  1. package/README.md +5 -2
  2. package/dist/customElement/index.d.ts.map +1 -1
  3. package/dist/customElement/index.js +23 -0
  4. package/dist/experimental/index.d.ts +1 -0
  5. package/dist/experimental/index.d.ts.map +1 -1
  6. package/dist/experimental/index.js +1 -0
  7. package/dist/experimental/machine/machine.d.ts +22 -3
  8. package/dist/experimental/machine/machine.d.ts.map +1 -1
  9. package/dist/experimental/machine/machine.js +8 -0
  10. package/dist/experimental/server/entry.d.ts +73 -0
  11. package/dist/experimental/server/entry.d.ts.map +1 -0
  12. package/dist/experimental/server/entry.js +41 -0
  13. package/dist/experimental/server/host.d.ts +42 -0
  14. package/dist/experimental/server/host.d.ts.map +1 -0
  15. package/dist/experimental/server/host.js +169 -0
  16. package/dist/experimental/server/index.d.ts +5 -0
  17. package/dist/experimental/server/index.d.ts.map +1 -0
  18. package/dist/experimental/server/index.js +4 -0
  19. package/dist/experimental/server/public.d.ts +3 -0
  20. package/dist/experimental/server/public.d.ts.map +1 -0
  21. package/dist/experimental/server/public.js +1 -0
  22. package/dist/experimental/server/serialize.d.ts +33 -0
  23. package/dist/experimental/server/serialize.d.ts.map +1 -0
  24. package/dist/experimental/server/serialize.js +563 -0
  25. package/dist/experimental/server/server.d.ts +201 -0
  26. package/dist/experimental/server/server.d.ts.map +1 -0
  27. package/dist/experimental/server/server.js +423 -0
  28. package/dist/experimental/server/template.d.ts +45 -0
  29. package/dist/experimental/server/template.d.ts.map +1 -0
  30. package/dist/experimental/server/template.js +182 -0
  31. package/dist/html/index.d.ts +5 -0
  32. package/dist/html/index.d.ts.map +1 -1
  33. package/dist/html/index.js +38 -4
  34. package/dist/hydrate.d.ts +4 -0
  35. package/dist/hydrate.d.ts.map +1 -0
  36. package/dist/hydrate.js +522 -0
  37. package/dist/hydrationMarker.d.ts +10 -0
  38. package/dist/hydrationMarker.d.ts.map +1 -0
  39. package/dist/hydrationMarker.js +9 -0
  40. package/dist/mount/index.d.ts +1 -1
  41. package/dist/runtime/public.d.ts +2 -2
  42. package/dist/runtime/public.d.ts.map +1 -1
  43. package/dist/runtime/public.js +1 -1
  44. package/dist/runtime/runtime.d.ts +75 -36
  45. package/dist/runtime/runtime.d.ts.map +1 -1
  46. package/dist/runtime/runtime.js +209 -71
  47. package/dist/snabbdom/h.d.ts +1 -0
  48. package/dist/snabbdom/h.d.ts.map +1 -1
  49. package/dist/snabbdom/h.js +85 -4
  50. package/dist/snabbdom/tovnode.d.ts.map +1 -1
  51. package/dist/snabbdom/tovnode.js +5 -1
  52. package/dist/tagName.d.ts +6 -0
  53. package/dist/tagName.d.ts.map +1 -0
  54. package/dist/tagName.js +11 -0
  55. package/dist/vdom.d.ts.map +1 -1
  56. package/dist/vdom.js +25 -1
  57. package/package.json +11 -4
@@ -58,6 +58,16 @@ const keyboardModifiers = (event) => ({
58
58
  /** Text direction for the document root, applied to `dir` on the `<html>`
59
59
  * element. `Auto` defers to the browser's first-strong-character heuristic. */
60
60
  export const TextDirection = S.Literals(['Ltr', 'Rtl', 'Auto']);
61
+ const textDirectionAttributes = {
62
+ Ltr: 'ltr',
63
+ Rtl: 'rtl',
64
+ Auto: 'auto',
65
+ };
66
+ /** Maps a {@link TextDirection} to the lowercase value written to the `dir`
67
+ * attribute on the `<html>` element. Shared by the client runtime, which sets
68
+ * it after each render, and server rendering, which stamps it into the served
69
+ * shell so the direction is correct on first paint. */
70
+ export const textDirectionToAttribute = (direction) => textDirectionAttributes[direction];
61
71
  const onMountStates = new WeakMap();
62
72
  // NOTE: snabbdom `destroy` hooks fire during the patch that removes an element,
63
73
  // and the hook on the OLD (being-removed) VNode was built in a prior live
@@ -197,6 +207,30 @@ const classObjectFor = (value) => {
197
207
  classObjectCache.set(value, classObject);
198
208
  return classObject;
199
209
  };
210
+ // NOTE: navigation and resource URL attributes (href, src, action,
211
+ // formaction) execute script when their scheme is `javascript:` or
212
+ // `vbscript:`, so an untrusted value bound to them is an XSS sink. Browsers
213
+ // ignore ASCII control characters embedded in a scheme (`java\tscript:`
214
+ // still runs), so those are stripped before the scheme is read. A dangerous
215
+ // scheme neutralizes to an empty value; every other URL, including relative
216
+ // paths, http(s), mailto, tel, and data URLs, passes through unchanged.
217
+ const DANGEROUS_URL_SCHEMES = new Set([
218
+ 'javascript',
219
+ 'vbscript',
220
+ ]);
221
+ const URL_CONTROL_CHARACTERS = /[\u0000-\u001F\u007F-\u009F]/g;
222
+ const URL_SCHEME_PATTERN = /^\s*([a-zA-Z][a-zA-Z0-9+.-]*)\s*:/;
223
+ const sanitizeUrl = (value) => {
224
+ const match = URL_SCHEME_PATTERN.exec(value.replace(URL_CONTROL_CHARACTERS, ''));
225
+ if (match !== null) {
226
+ const scheme = match[1];
227
+ if (scheme !== undefined &&
228
+ DANGEROUS_URL_SCHEMES.has(scheme.toLowerCase())) {
229
+ return '';
230
+ }
231
+ }
232
+ return value;
233
+ };
200
234
  const attributeHandlers = {
201
235
  Key: ({ value }, ctx) => setData(ctx, 'key', value),
202
236
  Class: ({ value }, ctx) => setModuleData(ctx, 'class', classObjectFor(value), VNodeDataMask.Class),
@@ -511,17 +545,17 @@ const attributeHandlers = {
511
545
  Min: ({ value }, ctx) => setDataProp(ctx, 'min', value),
512
546
  Step: ({ value }, ctx) => setDataProp(ctx, 'step', value),
513
547
  For: ({ value }, ctx) => setDataProp(ctx, 'htmlFor', value),
514
- Href: ({ value }, ctx) => setDataProp(ctx, 'href', value),
515
- Src: ({ value }, ctx) => setDataProp(ctx, 'src', value),
548
+ Href: ({ value }, ctx) => setDataProp(ctx, 'href', sanitizeUrl(value)),
549
+ Src: ({ value }, ctx) => setDataProp(ctx, 'src', sanitizeUrl(value)),
516
550
  Alt: ({ value }, ctx) => setDataProp(ctx, 'alt', value),
517
551
  Target: ({ value }, ctx) => setDataProp(ctx, 'target', value),
518
552
  Rel: ({ value }, ctx) => setDataProp(ctx, 'rel', value),
519
553
  Download: ({ value }, ctx) => setDataProp(ctx, 'download', value),
520
- Action: ({ value }, ctx) => setDataProp(ctx, 'action', value),
554
+ Action: ({ value }, ctx) => setDataProp(ctx, 'action', sanitizeUrl(value)),
521
555
  Method: ({ value }, ctx) => setDataProp(ctx, 'method', value),
522
556
  Enctype: ({ value }, ctx) => setDataProp(ctx, 'enctype', value),
523
557
  Novalidate: ({ value }, ctx) => setDataProp(ctx, 'noValidate', value),
524
- Formaction: ({ value }, ctx) => setDataProp(ctx, 'formAction', value),
558
+ Formaction: ({ value }, ctx) => setDataProp(ctx, 'formAction', sanitizeUrl(value)),
525
559
  Formmethod: ({ value }, ctx) => setDataProp(ctx, 'formMethod', value),
526
560
  Formnovalidate: ({ value }, ctx) => setDataProp(ctx, 'formNoValidate', value),
527
561
  Formtarget: ({ value }, ctx) => setDataProp(ctx, 'formTarget', value),
@@ -0,0 +1,4 @@
1
+ import type { VNode } from './snabbdom/index.js';
2
+ export declare const __elementSignature: (element: Element, vnode: VNode) => string;
3
+ export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen?: Set<object>) => VNode;
4
+ //# sourceMappingURL=hydrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAsJhD,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,EAAE,OAAO,KAAK,KAAG,MA6BnE,CAAA;AAsbD,eAAO,MAAM,cAAc,GACzB,eAAe,OAAO,EACtB,WAAW,KAAK,GAAG,IAAI,EACvB,OAAO,GAAG,CAAC,MAAM,CAAC,KACjB,KAwCF,CAAA"}
@@ -0,0 +1,522 @@
1
+ import { Option } from 'effect';
2
+ import { h, toVNode } from './snabbdom/index.js';
3
+ import { tagNameFromSelector } from './tagName.js';
4
+ import { dedupeSharedVNodes, patch } from './vdom.js';
5
+ // NOTE: the differ only knows the page through the `elm` pointers on its
6
+ // vnodes; `patch` never queries the document. On a server-rendered page the
7
+ // browser has already built real DOM by parsing the HTML the server sent
8
+ // (the "server DOM" below), but snabbdom did not create those nodes, so no
9
+ // vnode anywhere points at them and the differ cannot see them. Hydration's
10
+ // whole job is to make that DOM visible to the differ, then let one
11
+ // ordinary patch attach behavior to it.
12
+ //
13
+ // The mechanism has three steps. First, walk the first render's vnode tree
14
+ // and the server DOM together, position by position. Second, wherever the
15
+ // two agree, record the existing DOM node: those records form a second
16
+ // vnode tree, a clone of the first render's tree whose `elm` fields point
17
+ // at the server DOM nodes. The clone copies exactly what `sameVnode`
18
+ // compares (`sel`, `key`, `identity`, `data.is`), so `patchVnode` reuses
19
+ // every adopted element, and deliberately nothing else, so every module
20
+ // update hook re-asserts the new tree's attrs, props, classes, styles, and
21
+ // listeners onto the adopted elements. Third, run `patch(clone, newTree)`:
22
+ // to the differ this is a completely ordinary update from a tree that
23
+ // happens to point at existing nodes, so the vendored differ stays
24
+ // untouched and never learns the nodes came from a server.
25
+ //
26
+ // Where the DOM disagrees with the vnode tree, the walk clears the nearest
27
+ // parent's children and hands `patch` an empty child list, so the subtree is
28
+ // rebuilt through `createElm`, which is exactly the pre-hydration replace
29
+ // behavior scoped to the mismatching subtree. Trailing vnode children with
30
+ // no DOM counterpart are simply absent from the clone; `updateChildren`
31
+ // appends them.
32
+ const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
33
+ const HYDRATION_STAMP_ATTRIBUTE = 'data-foldkit-app';
34
+ // The style module keys regular properties in camelCase (`backgroundColor`)
35
+ // and custom properties as written (`--accent`), so a property read from the
36
+ // DOM in kebab case is converted to the module's key form before seeding.
37
+ const styleModuleKey = (property) => property.startsWith('--')
38
+ ? property
39
+ : property.replace(/-([a-z])/g, (_match, character) => character.toUpperCase());
40
+ const classListOf = (element) => {
41
+ const classes = {};
42
+ for (const className of Array.from(element.classList)) {
43
+ classes[className] = true;
44
+ }
45
+ return classes;
46
+ };
47
+ const inlineStyleOf = (element) => {
48
+ const style = {};
49
+ if (element instanceof HTMLElement || element instanceof SVGElement) {
50
+ const inlineStyle = element.style;
51
+ for (let index = 0; index < inlineStyle.length; index += 1) {
52
+ const property = inlineStyle.item(index);
53
+ style[styleModuleKey(property)] = inlineStyle.getPropertyValue(property);
54
+ }
55
+ }
56
+ return style;
57
+ };
58
+ // NOTE: for a custom element, seed only the class tokens the vnode declares so
59
+ // the class module leaves component-added tokens in place; the view does not
60
+ // reassert them, so the full class list would reconcile them away as stale.
61
+ // Reading each token from the element keeps an agreeing render a no-op. A normal
62
+ // element seeds its whole class list, so stale tokens reconcile away.
63
+ const seedClasses = (element, vnode, classOwnedByModule, isCustomElement) => {
64
+ if (!classOwnedByModule) {
65
+ return {};
66
+ }
67
+ if (!isCustomElement) {
68
+ return classListOf(element);
69
+ }
70
+ const declared = {};
71
+ for (const token of Object.keys(vnode.data?.class ?? {})) {
72
+ declared[token] = element.classList.contains(token);
73
+ }
74
+ return declared;
75
+ };
76
+ // NOTE: the style counterpart to seedClasses. For a custom element, seed only
77
+ // the style properties the vnode declares so the style module leaves
78
+ // component-added properties in place; a normal element seeds its whole inline
79
+ // style so stale properties reconcile away.
80
+ const seedStyle = (element, vnode, styleOwnedByModule, isCustomElement) => {
81
+ if (!styleOwnedByModule) {
82
+ return {};
83
+ }
84
+ if (!isCustomElement) {
85
+ return inlineStyleOf(element);
86
+ }
87
+ const current = inlineStyleOf(element);
88
+ const declared = {};
89
+ for (const [key, value] of Object.entries(vnode.data?.style ?? {})) {
90
+ if (typeof value === 'string') {
91
+ declared[key] = current[key] ?? '';
92
+ }
93
+ }
94
+ return declared;
95
+ };
96
+ // Properties the serializer emits as attributes but the client sets as DOM
97
+ // properties that do not reflect back to the attribute, so a correct hydration
98
+ // drops the server attribute. When a vnode owns one of these through
99
+ // `data.props`, the signature compares the live property value instead of the
100
+ // attribute, so that expected drop is not read as a disagreement. A view that
101
+ // instead sets the same name as a raw attribute keeps it in the attribute set.
102
+ const NON_REFLECTING_PROPERTIES = new Set([
103
+ 'value',
104
+ 'checked',
105
+ 'selected',
106
+ 'muted',
107
+ ]);
108
+ const propertyManagedNames = (vnode) => {
109
+ const props = vnode.data?.props;
110
+ if (props === undefined) {
111
+ return [];
112
+ }
113
+ return Array.from(NON_REFLECTING_PROPERTIES).filter(name => name in props);
114
+ };
115
+ const byName = ([leftName], [rightName]) => leftName.localeCompare(rightName);
116
+ // A structured, order-independent snapshot of the state an element and its
117
+ // vnode share: the DOM attributes, the non-reflecting properties the vnode
118
+ // owns (read from the element, not the attribute), the class set, and inline
119
+ // style. Comparing the snapshot taken during adoption (the server DOM) with
120
+ // the element's state after the client patch flags any attribute, property,
121
+ // class, or style the two disagree on. Both sides pass through the same DOM
122
+ // APIs, so spelling differences never register, and the snapshot is JSON so no
123
+ // value can collide with a delimiter. Values feed the comparison, never a log.
124
+ export const __elementSignature = (element, vnode) => {
125
+ const propertyNames = propertyManagedNames(vnode);
126
+ const attributes = [];
127
+ for (const attribute of Array.from(element.attributes)) {
128
+ const name = attribute.name;
129
+ if (name === HYDRATION_STAMP_ATTRIBUTE ||
130
+ name === 'class' ||
131
+ name === 'style' ||
132
+ propertyNames.includes(name)) {
133
+ continue;
134
+ }
135
+ attributes.push([name, attribute.value]);
136
+ }
137
+ attributes.sort(byName);
138
+ const properties = [];
139
+ for (const name of propertyNames) {
140
+ properties.push([name, String(Reflect.get(element, name))]);
141
+ }
142
+ properties.sort(byName);
143
+ const styles = Object.entries(inlineStyleOf(element)).sort(byName);
144
+ const classes = Array.from(element.classList).sort();
145
+ return JSON.stringify({ attributes, properties, classes, styles });
146
+ };
147
+ // NOTE: reconcile stale server DOM against the client's first render by
148
+ // seeding the adopted clone with the element's current attributes, classes,
149
+ // and inline styles. Server DOM state is all view-produced, so the diff
150
+ // modules remove any value the client tree does not reassert, converging a
151
+ // nondeterministic render instead of leaving stale, behavior-affecting state
152
+ // (a stale href, a stale class) on the page. class and inline style are
153
+ // seeded into their own module only when the client view owns them solely
154
+ // through that module. When the view also sets `class` or `style` through a
155
+ // raw attribute, or does not use the module at all, the whole attribute rides
156
+ // in attrs and the module (if present) re-asserts its tokens on top, so no
157
+ // value is written by one module and then removed as stale state by another.
158
+ // The hydration stamp is never seeded, so it is never removed.
159
+ const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
160
+ const classOwnedByModule = vnode.data?.class !== undefined &&
161
+ vnode.data?.attrs?.['class'] === undefined;
162
+ const styleOwnedByModule = vnode.data?.style !== undefined &&
163
+ vnode.data?.attrs?.['style'] === undefined;
164
+ // NOTE: a custom element that upgraded before hydration adds attributes of its
165
+ // own in connectedCallback. Seeding only the attributes the vnode declares
166
+ // leaves those component-owned attributes in place, while a vnode-declared
167
+ // attribute still reconciles.
168
+ const declaredAttributes = isCustomElement
169
+ ? new Set(Object.keys(vnode.data?.attrs ?? {}).map(name => name.toLowerCase()))
170
+ : undefined;
171
+ const attrs = {};
172
+ for (const attribute of Array.from(element.attributes)) {
173
+ const name = attribute.name;
174
+ if (name === HYDRATION_STAMP_ATTRIBUTE) {
175
+ continue;
176
+ }
177
+ if (name === 'class' && classOwnedByModule) {
178
+ continue;
179
+ }
180
+ if (name === 'style' && styleOwnedByModule) {
181
+ continue;
182
+ }
183
+ if (declaredAttributes !== undefined &&
184
+ !declaredAttributes.has(name.toLowerCase())) {
185
+ continue;
186
+ }
187
+ attrs[name] = attribute.value;
188
+ }
189
+ const classes = seedClasses(element, vnode, classOwnedByModule, isCustomElement);
190
+ const style = seedStyle(element, vnode, styleOwnedByModule, isCustomElement);
191
+ clone.data = {
192
+ ...clone.data,
193
+ ...(Object.keys(attrs).length > 0 ? { attrs } : {}),
194
+ ...(Object.keys(classes).length > 0 ? { class: classes } : {}),
195
+ ...(Object.keys(style).length > 0 ? { style } : {}),
196
+ };
197
+ // In development, record the server DOM signature so the post-patch pass can
198
+ // report an attribute-only mismatch the structural walk cannot see. Gated on
199
+ // the dev flag so a production hydrate does no extra work.
200
+ if (import.meta.hot) {
201
+ status.adoptedSignatures.set(element, {
202
+ vnode,
203
+ server: __elementSignature(element, vnode),
204
+ });
205
+ }
206
+ };
207
+ const detectMismatch = (status) => {
208
+ status.isMismatchDetected = true;
209
+ };
210
+ const reportMismatch = (status) => {
211
+ if (import.meta.hot && status.isMismatchDetected) {
212
+ console.warn('[foldkit] The server DOM did not match the first client view during ' +
213
+ 'hydration. Foldkit reconciled the mismatching subtree. Ensure Flags, ' +
214
+ 'init, and view produce deterministic initial markup.');
215
+ }
216
+ };
217
+ const isText = (node) => node.nodeType === Node.TEXT_NODE;
218
+ const isComment = (node) => node.nodeType === Node.COMMENT_NODE;
219
+ const isElement = (node) => node.nodeType === Node.ELEMENT_NODE;
220
+ const hasOnlyTextContent = (element) => {
221
+ const firstChild = element.firstChild;
222
+ return (firstChild === null ||
223
+ (firstChild.nextSibling === null && isText(firstChild)));
224
+ };
225
+ const matchesTag = (element, selector) => element.tagName.toLowerCase() === tagNameFromSelector(selector).toLowerCase();
226
+ // The namespace a vnode expects is carried in `data.ns` for foreign content
227
+ // (SVG, MathML) and is otherwise HTML. An element whose namespace disagrees
228
+ // (an HTML element parsed inside an SVG integration point, say) must be
229
+ // rebuilt rather than adopted, since the two are not interchangeable.
230
+ const namespaceOf = (vnode) => typeof vnode.data?.ns === 'string' ? vnode.data.ns : HTML_NAMESPACE;
231
+ const matchesNamespace = (element, vnode) => (element.namespaceURI ?? HTML_NAMESPACE) === namespaceOf(vnode);
232
+ const cloneOf = (vnode, elm) => {
233
+ const clone = {
234
+ sel: vnode.sel,
235
+ data: vnode.data?.is === undefined ? {} : { is: vnode.data.is },
236
+ children: undefined,
237
+ elm,
238
+ text: undefined,
239
+ key: vnode.key,
240
+ };
241
+ if (vnode.identity !== undefined) {
242
+ clone.identity = vnode.identity;
243
+ }
244
+ return clone;
245
+ };
246
+ const asVNode = (child) => typeof child === 'string'
247
+ ? {
248
+ sel: undefined,
249
+ data: undefined,
250
+ children: undefined,
251
+ elm: undefined,
252
+ text: child,
253
+ key: undefined,
254
+ }
255
+ : child;
256
+ const clearChildren = (element) => {
257
+ element.textContent = '';
258
+ };
259
+ const adoptText = (element, domChild, text) => {
260
+ if (domChild !== null && isText(domChild)) {
261
+ const domText = domChild.data;
262
+ if (domText === text) {
263
+ return Option.some({
264
+ adoptedNode: domChild,
265
+ nextDomChild: domChild.nextSibling,
266
+ });
267
+ }
268
+ if (domText.startsWith(text)) {
269
+ domChild.splitText(text.length);
270
+ return Option.some({
271
+ adoptedNode: domChild,
272
+ nextDomChild: domChild.nextSibling,
273
+ });
274
+ }
275
+ return Option.none();
276
+ }
277
+ if (text === '') {
278
+ const emptyTextNode = element.ownerDocument.createTextNode('');
279
+ element.insertBefore(emptyTextNode, domChild);
280
+ return Option.some({ adoptedNode: emptyTextNode, nextDomChild: domChild });
281
+ }
282
+ return Option.none();
283
+ };
284
+ const adoptElement = (element, vnode, adopted, status) => {
285
+ const clone = cloneOf(vnode, element);
286
+ adopted.add(element);
287
+ // NOTE: an autonomous custom element (an HTML-namespace element whose name
288
+ // carries a hyphen) that upgraded before hydration adds attributes, classes,
289
+ // styles, and light DOM of its own in connectedCallback. The attributes, class
290
+ // tokens, and style properties the vnode does not declare are always preserved
291
+ // (here and in seedAdoptedState). Light DOM ownership follows the vnode: a
292
+ // childless vnode leaves the component's light DOM untouched, while a vnode
293
+ // that declares children owns the light DOM and reconciles it like any
294
+ // element. The two cannot share: once both write same-tag nodes a positional
295
+ // walk cannot tell a component node from a view node, so declared children
296
+ // take full ownership rather than interleave. The test is the name shape, not
297
+ // `customElements.get`: whether the element has upgraded is timing-dependent
298
+ // at hydration (its definition may register after the server DOM parses), so a
299
+ // name test is deterministic. A hyphenated element that never upgrades is
300
+ // treated the same way, which is safe: with no component light DOM, a
301
+ // childless vnode leaves an empty element and a vnode with children reconciles
302
+ // normally.
303
+ const isCustomElement = (element.namespaceURI === null ||
304
+ element.namespaceURI === HTML_NAMESPACE) &&
305
+ element.localName.includes('-');
306
+ seedAdoptedState(element, vnode, clone, status, isCustomElement);
307
+ // NOTE: a controlled textarea serializes its value as text content, which
308
+ // sets the element's defaultValue. A fresh boot sets only the value
309
+ // property and leaves defaultValue empty, so the server text is cleared
310
+ // before the props module applies the value; otherwise the adopted
311
+ // textarea's defaultValue and form.reset would differ from a fresh boot.
312
+ // An uncontrolled textarea, whose content is its default, is left alone.
313
+ if (element.tagName === 'TEXTAREA' &&
314
+ vnode.data?.props?.['value'] !== undefined) {
315
+ clearChildren(element);
316
+ clone.children = [];
317
+ return clone;
318
+ }
319
+ const authoredInnerHtml = vnode.data?.props?.['innerHTML'];
320
+ if (authoredInnerHtml !== undefined) {
321
+ // NOTE: the browser normalizes markup as it parses (entity forms, tag
322
+ // case, attribute order), so the served innerHTML string rarely equals
323
+ // the authored one byte for byte. Parsing the authored string through a
324
+ // probe element of the same tag compares the two in normalized form;
325
+ // when they agree the clone carries the authored string, the props
326
+ // module sees no change, and the adopted subtree survives. The probe is
327
+ // created in the element's own namespace: foreign content such as SVG
328
+ // parses with case-preserved names (pathLength, viewBox) that an
329
+ // HTML-context parse would lowercase, false-mismatching every camelCase
330
+ // attribute.
331
+ if (typeof authoredInnerHtml === 'string') {
332
+ const probe = element.namespaceURI === null || element.namespaceURI === HTML_NAMESPACE
333
+ ? element.ownerDocument.createElement(element.tagName)
334
+ : element.ownerDocument.createElementNS(element.namespaceURI, element.tagName);
335
+ probe.innerHTML = authoredInnerHtml;
336
+ const isEquivalentMarkup = probe.innerHTML === element.innerHTML;
337
+ if (!isEquivalentMarkup) {
338
+ detectMismatch(status);
339
+ }
340
+ clone.data = {
341
+ ...clone.data,
342
+ props: {
343
+ innerHTML: isEquivalentMarkup ? authoredInnerHtml : element.innerHTML,
344
+ },
345
+ };
346
+ }
347
+ else {
348
+ clone.data = { ...clone.data, props: { innerHTML: element.innerHTML } };
349
+ }
350
+ clone.children = [];
351
+ return clone;
352
+ }
353
+ const vnodeChildren = vnode.children;
354
+ // NOTE: no children (undefined) and an empty child list both mean the view
355
+ // declares no children, so they share the childless path. This is also where
356
+ // a custom element's ownership splits: a childless vnode has no view child to
357
+ // adopt, so the component's light DOM is left untouched, while a vnode with
358
+ // real children (the branch below) owns the light DOM and reconciles it.
359
+ if (vnodeChildren === undefined || vnodeChildren.length === 0) {
360
+ // NOTE: only adopt the text shortcut when the element already holds a
361
+ // single text node. `textContent` flattens across element children, so
362
+ // copying it for an element that carries stray markup would compare equal
363
+ // to the vnode text and leave that markup in place. Leaving `clone.text`
364
+ // undefined makes `patchVnode` overwrite the element's content with the
365
+ // vnode text instead, rebuilding the mismatching shape.
366
+ if (vnode.text !== undefined) {
367
+ if (hasOnlyTextContent(element)) {
368
+ clone.text = element.textContent ?? '';
369
+ if (clone.text !== vnode.text) {
370
+ detectMismatch(status);
371
+ }
372
+ }
373
+ else {
374
+ detectMismatch(status);
375
+ }
376
+ }
377
+ else if (!isCustomElement && element.firstChild !== null) {
378
+ // NOTE: a childless vnode (no children or an empty child list, and no
379
+ // text) owns an empty element. Server DOM left under it, an older build's
380
+ // content behind a cache race, would otherwise survive every future
381
+ // render, since patchVnode has nothing to diff it against. Clear it so the
382
+ // empty client tree wins, matching the mismatch branches below. A custom
383
+ // element is exempt: its light DOM is component-owned, not stale server
384
+ // state.
385
+ detectMismatch(status);
386
+ clearChildren(element);
387
+ clone.children = [];
388
+ }
389
+ return clone;
390
+ }
391
+ const cloneChildren = [];
392
+ let domChild = element.firstChild;
393
+ for (const rawChild of vnodeChildren) {
394
+ const child = asVNode(rawChild);
395
+ if (child.sel === undefined || child.sel === '') {
396
+ const childText = child.text ?? '';
397
+ const maybeAdoption = adoptText(element, domChild, childText);
398
+ if (Option.isNone(maybeAdoption)) {
399
+ detectMismatch(status);
400
+ if (domChild === null) {
401
+ break;
402
+ }
403
+ clearChildren(element);
404
+ clone.children = [];
405
+ return clone;
406
+ }
407
+ const adoption = maybeAdoption.value;
408
+ const textClone = cloneOf(child, adoption.adoptedNode);
409
+ textClone.text = childText;
410
+ cloneChildren.push(textClone);
411
+ domChild = adoption.nextDomChild;
412
+ continue;
413
+ }
414
+ if (domChild === null) {
415
+ detectMismatch(status);
416
+ break;
417
+ }
418
+ if (child.sel === '!') {
419
+ if (!isComment(domChild)) {
420
+ detectMismatch(status);
421
+ clearChildren(element);
422
+ clone.children = [];
423
+ return clone;
424
+ }
425
+ const commentClone = cloneOf(child, domChild);
426
+ commentClone.text = domChild.data;
427
+ cloneChildren.push(commentClone);
428
+ domChild = domChild.nextSibling;
429
+ continue;
430
+ }
431
+ if (!isElement(domChild) ||
432
+ !matchesTag(domChild, child.sel) ||
433
+ !matchesNamespace(domChild, child)) {
434
+ detectMismatch(status);
435
+ clearChildren(element);
436
+ clone.children = [];
437
+ return clone;
438
+ }
439
+ cloneChildren.push(adoptElement(domChild, child, adopted, status));
440
+ domChild = domChild.nextSibling;
441
+ }
442
+ while (domChild !== null) {
443
+ detectMismatch(status);
444
+ const nextDomChild = domChild.nextSibling;
445
+ element.removeChild(domChild);
446
+ domChild = nextDomChild;
447
+ }
448
+ clone.children = cloneChildren;
449
+ return clone;
450
+ };
451
+ const fireAdoptedInsertHooks = (vnode, adopted) => {
452
+ const children = vnode.children;
453
+ if (children !== undefined) {
454
+ for (const child of children) {
455
+ if (typeof child !== 'string') {
456
+ fireAdoptedInsertHooks(child, adopted);
457
+ }
458
+ }
459
+ }
460
+ const insertHook = vnode.data?.hook?.insert;
461
+ if (insertHook !== undefined &&
462
+ vnode.elm !== undefined &&
463
+ adopted.has(vnode.elm)) {
464
+ insertHook(vnode);
465
+ }
466
+ };
467
+ /** Hydrates a server-rendered root element against the first render's vnode
468
+ * tree. Matching DOM nodes are adopted in place, so pre-rendered content is
469
+ * never torn down on boot: module hooks attach listeners and re-assert
470
+ * attrs and props onto the existing elements, and `insert` hooks (Mounts)
471
+ * fire for adopted nodes in the same children-first order the differ uses
472
+ * for created ones. A mismatching subtree falls back to a rebuild through
473
+ * `createElm` at the nearest parent, and a root-level mismatch falls back
474
+ * to the pre-hydration replace boot. Development builds warn when
475
+ * reconciliation is required. Returns the patched vnode to store as the
476
+ * runtime's current tree. */
477
+ // Replace the hydration root with a fresh render of the vnode. snabbdom's
478
+ // sameVnode compares tag but not namespace, so patching the root directly
479
+ // would reuse a same-tag element even across a namespace change. Patching
480
+ // against a comment placed where the root was is never sameVnode with a new
481
+ // element, so the differ builds a fresh node in the correct namespace and
482
+ // swaps it in.
483
+ const replaceHydrationRoot = (hydrationRoot, vnode) => {
484
+ const parent = hydrationRoot.parentNode;
485
+ if (parent === null) {
486
+ return patch(toVNode(hydrationRoot), vnode);
487
+ }
488
+ const placeholder = hydrationRoot.ownerDocument.createComment('');
489
+ parent.replaceChild(placeholder, hydrationRoot);
490
+ return patch(toVNode(placeholder), vnode);
491
+ };
492
+ export const __hydrateVNode = (hydrationRoot, nextVNode, seen) => {
493
+ const dedupedVNode = nextVNode !== null ? dedupeSharedVNodes(nextVNode, seen) : h('!');
494
+ const status = {
495
+ isMismatchDetected: false,
496
+ adoptedSignatures: new Map(),
497
+ };
498
+ if (dedupedVNode.sel === undefined ||
499
+ dedupedVNode.sel === '' ||
500
+ dedupedVNode.sel === '!' ||
501
+ !matchesTag(hydrationRoot, dedupedVNode.sel) ||
502
+ !matchesNamespace(hydrationRoot, dedupedVNode)) {
503
+ detectMismatch(status);
504
+ const patchedVNode = replaceHydrationRoot(hydrationRoot, dedupedVNode);
505
+ reportMismatch(status);
506
+ return patchedVNode;
507
+ }
508
+ const adopted = new Set();
509
+ const adoptedClone = adoptElement(hydrationRoot, dedupedVNode, adopted, status);
510
+ const patchedVNode = patch(adoptedClone, dedupedVNode);
511
+ if (import.meta.hot && !status.isMismatchDetected) {
512
+ for (const [element, { vnode, server }] of status.adoptedSignatures) {
513
+ if (__elementSignature(element, vnode) !== server) {
514
+ detectMismatch(status);
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ fireAdoptedInsertHooks(patchedVNode, adopted);
520
+ reportMismatch(status);
521
+ return patchedVNode;
522
+ };
@@ -0,0 +1,10 @@
1
+ /** Attribute stamped on the server-rendered application root. Its presence
2
+ * tells a booting runtime to hydrate instead of rendering fresh, and its
3
+ * value is the runtime id used for HMR model preservation. */
4
+ export declare const FOLDKIT_APP_ATTRIBUTE = "data-foldkit-app";
5
+ /** Attribute on the JSON script tag carrying the Schema-encoded flags the
6
+ * server rendered with. A hydrating runtime decodes this payload instead of
7
+ * running the client `flags` Effect, so both sides call `init` with the
8
+ * same value. */
9
+ export declare const FOLDKIT_FLAGS_ATTRIBUTE = "data-foldkit-flags";
10
+ //# sourceMappingURL=hydrationMarker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrationMarker.d.ts","sourceRoot":"","sources":["../src/hydrationMarker.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAC/D,eAAO,MAAM,qBAAqB,qBAAqB,CAAA;AAEvD;;;kBAGkB;AAClB,eAAO,MAAM,uBAAuB,uBAAuB,CAAA"}
@@ -0,0 +1,9 @@
1
+ /** Attribute stamped on the server-rendered application root. Its presence
2
+ * tells a booting runtime to hydrate instead of rendering fresh, and its
3
+ * value is the runtime id used for HMR model preservation. */
4
+ export const FOLDKIT_APP_ATTRIBUTE = 'data-foldkit-app';
5
+ /** Attribute on the JSON script tag carrying the Schema-encoded flags the
6
+ * server rendered with. A hydrating runtime decodes this payload instead of
7
+ * running the client `flags` Effect, so both sides call `init` with the
8
+ * same value. */
9
+ export const FOLDKIT_FLAGS_ATTRIBUTE = 'data-foldkit-flags';
@@ -133,7 +133,7 @@ export type MountDefinition<Name extends string = string, ResultMessage = any> =
133
133
  * )(({ buttonId, anchor }) => element =>
134
134
  * Effect.gen(function* () {
135
135
  * yield* Effect.acquireRelease(
136
- * Effect.sync(() => anchorSetup({ buttonId, anchor })(element)),
136
+ * Effect.sync(() => anchorSetup(element, { buttonId, anchor })),
137
137
  * cleanup => Effect.sync(cleanup),
138
138
  * )
139
139
  * return CompletedAnchorPopover()
@@ -1,4 +1,4 @@
1
- export { SlowPhase, defaultSlowCallback, embed, makeApplication, makeElement, run, } from './runtime.js';
2
- export type { RoutingConfig, CrashConfig, CrashContext, ElementCrashConfig, RoutingApplicationConfigWithFlags, RoutingApplicationConfig, ApplicationConfigWithFlags, ApplicationConfig, ElementConfigWithFlags, ElementConfig, ApplicationInit, RoutingApplicationInit, ElementInit, EmbedHandle, InboundPortHandle, InboundPortHandles, OutboundPortHandle, OutboundPortHandles, PortHandles, MakeRuntimeReturn, Visibility, SlowConfig, SlowContext, SlowPatchContext, SlowSubscriptionDependenciesContext, SlowThresholdOverrides, SlowUpdateContext, SlowViewContext, DevToolsConfig, DevToolsMode, DevToolsModeConfig, DevToolsPosition, } from './runtime.js';
1
+ export { SlowPhase, defaultSlowCallback, embed, hydrate, makeApplication, makeElement, run, } from './runtime.js';
2
+ export type { RoutingConfig, CrashConfig, CrashContext, ElementCrashConfig, RoutingApplicationConfigWithFlags, RoutingApplicationConfig, ApplicationConfigWithFlags, ApplicationConfig, ElementConfigWithFlags, ElementConfig, ApplicationInit, RoutingApplicationInit, ElementInit, EmbedHandle, InboundPortHandle, InboundPortHandles, OutboundPortHandle, OutboundPortHandles, PortHandles, MakeRuntimeReturn, RunOptions, Visibility, SlowConfig, SlowContext, SlowPatchContext, SlowSubscriptionDependenciesContext, SlowThresholdOverrides, SlowUpdateContext, SlowViewContext, DevToolsConfig, DevToolsMode, DevToolsModeConfig, DevToolsPosition, } from './runtime.js';
3
3
  export type { ViewTransitionConfig, ViewTransitionContext, ViewTransitionDecision, } from './viewTransition.js';
4
4
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/runtime/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,KAAK,EACL,eAAe,EACf,WAAW,EACX,GAAG,GACJ,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,aAAa,EACb,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,iCAAiC,EACjC,wBAAwB,EACxB,0BAA0B,EAC1B,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,mCAAmC,EACnC,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/runtime/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,KAAK,EACL,OAAO,EACP,eAAe,EACf,WAAW,EACX,GAAG,GACJ,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,aAAa,EACb,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,iCAAiC,EACjC,wBAAwB,EACxB,0BAA0B,EAC1B,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,eAAe,EACf,sBAAsB,EACtB,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,UAAU,EACV,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,mCAAmC,EACnC,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,cAAc,CAAA;AAErB,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,qBAAqB,CAAA"}
@@ -1 +1 @@
1
- export { SlowPhase, defaultSlowCallback, embed, makeApplication, makeElement, run, } from './runtime.js';
1
+ export { SlowPhase, defaultSlowCallback, embed, hydrate, makeApplication, makeElement, run, } from './runtime.js';