foldkit 0.147.0 → 0.148.1

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 (63) hide show
  1. package/dist/buildToken.d.ts +3 -0
  2. package/dist/buildToken.d.ts.map +1 -0
  3. package/dist/buildToken.js +21 -0
  4. package/dist/controlledDomState.d.ts +25 -0
  5. package/dist/controlledDomState.d.ts.map +1 -0
  6. package/dist/controlledDomState.js +240 -0
  7. package/dist/cssStyleProperties.d.ts +6 -0
  8. package/dist/cssStyleProperties.d.ts.map +1 -0
  9. package/dist/cssStyleProperties.js +91 -0
  10. package/dist/domReflection.d.ts +73 -0
  11. package/dist/domReflection.d.ts.map +1 -0
  12. package/dist/domReflection.js +557 -0
  13. package/dist/experimental/server/host.d.ts +102 -8
  14. package/dist/experimental/server/host.d.ts.map +1 -1
  15. package/dist/experimental/server/host.js +203 -13
  16. package/dist/experimental/server/public.d.ts +2 -2
  17. package/dist/experimental/server/public.d.ts.map +1 -1
  18. package/dist/experimental/server/public.js +1 -1
  19. package/dist/experimental/server/serialize.d.ts +15 -5
  20. package/dist/experimental/server/serialize.d.ts.map +1 -1
  21. package/dist/experimental/server/serialize.js +373 -144
  22. package/dist/experimental/server/server.d.ts +55 -14
  23. package/dist/experimental/server/server.d.ts.map +1 -1
  24. package/dist/experimental/server/server.js +546 -22
  25. package/dist/experimental/server/template.d.ts +8 -0
  26. package/dist/experimental/server/template.d.ts.map +1 -1
  27. package/dist/experimental/server/template.js +437 -2
  28. package/dist/html/index.d.ts.map +1 -1
  29. package/dist/html/index.js +436 -29
  30. package/dist/hydrate.d.ts +1 -1
  31. package/dist/hydrate.d.ts.map +1 -1
  32. package/dist/hydrate.js +449 -121
  33. package/dist/hydrationMarkers.d.ts +15 -0
  34. package/dist/hydrationMarkers.d.ts.map +1 -0
  35. package/dist/hydrationMarkers.js +72 -0
  36. package/dist/nativeInnerHtml.d.ts +13 -0
  37. package/dist/nativeInnerHtml.d.ts.map +1 -0
  38. package/dist/nativeInnerHtml.js +30 -0
  39. package/dist/propertyProvenance.d.ts +33 -0
  40. package/dist/propertyProvenance.d.ts.map +1 -0
  41. package/dist/propertyProvenance.js +78 -0
  42. package/dist/propsModule.d.ts.map +1 -1
  43. package/dist/propsModule.js +149 -15
  44. package/dist/runtime/public.d.ts +1 -1
  45. package/dist/runtime/public.d.ts.map +1 -1
  46. package/dist/runtime/runtime.d.ts +33 -6
  47. package/dist/runtime/runtime.d.ts.map +1 -1
  48. package/dist/runtime/runtime.js +240 -27
  49. package/dist/snabbdom/attributes.d.ts.map +1 -1
  50. package/dist/snabbdom/attributes.js +65 -37
  51. package/dist/snabbdom/class.d.ts.map +1 -1
  52. package/dist/snabbdom/class.js +61 -5
  53. package/dist/snabbdom/style.d.ts.map +1 -1
  54. package/dist/snabbdom/style.js +57 -34
  55. package/dist/test/apps/attributes.d.ts +1 -0
  56. package/dist/test/apps/attributes.d.ts.map +1 -1
  57. package/dist/test/apps/attributes.js +8 -1
  58. package/dist/test/apps/login.js +1 -1
  59. package/dist/test/matchers.d.ts.map +1 -1
  60. package/dist/test/matchers.js +2 -1
  61. package/dist/test/scene.d.ts.map +1 -1
  62. package/dist/test/scene.js +2 -1
  63. package/package.json +2 -2
@@ -1,6 +1,10 @@
1
1
  import { Array, Context, Data, Effect, Fiber, Function, Option, Schema as S, Stream, } from 'effect';
2
+ import { restoreUncontrolledContent, synchronizeControlledDefault, } from '../controlledDomState.js';
3
+ import { FOREIGN_REPRESENTABLE_PROPERTIES, assertStyleIsRepresentable, htmlAttributeValue, isHtmlPropertyRepresentable, normalizedStyleProperties, reflectedAttributeName, } from '../domReflection.js';
2
4
  import { MountTracker } from '../mount/index.js';
5
+ import { hasTrustedInnerHtml, isClientOnlyProperty, markClientOnlyProperty, markTrustedInnerHtml, unmarkClientOnlyProperty, } from '../propertyProvenance.js';
3
6
  import { VNodeDataMask, h, vnodeDataMaskKey, } from '../snabbdom/index.js';
7
+ import { tagNameFromSelector } from '../tagName.js';
4
8
  import { isChildAttribute } from './childAttribute.js';
5
9
  import { checkScheduledLeave, clearDragZoneAfterDrop, getDragZoneState, processDragEnter, processDragLeave, } from './dragZoneTracking.js';
6
10
  import { clearRuntime, requireBoundaryMappers, requireDispatch, requireRuntimeContext, requireUnmountResolver, setRuntime, } from './runtimeSingleton.js';
@@ -110,11 +114,45 @@ const setModuleData = (ctx, key, value, dataMask) => {
110
114
  // NOTE: single-key fast paths. The bulk of attribute handlers set exactly
111
115
  // one prop, attr, or event handler; writing it directly into the vnode data
112
116
  // avoids allocating a `{ key: value }` literal and merging it per attribute.
113
- const setDataProp = (ctx, key, value) => {
117
+ const writeDataProp = (ctx, key, value) => {
114
118
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
115
119
  const props = (ctx.data.props ??= {});
116
- props[key] = value;
120
+ Object.defineProperty(props, key, {
121
+ configurable: true,
122
+ enumerable: true,
123
+ value,
124
+ writable: true,
125
+ });
117
126
  addVNodeDataMask(ctx, VNodeDataMask.Props);
127
+ return props;
128
+ };
129
+ // A typed attribute builder writes the DOM property that reflects the HTML
130
+ // attribute it is named after, so the server serializer can emit it as that
131
+ // attribute even on a custom element, where a same-named component property
132
+ // would mean something else entirely. Overwriting a property a generic write
133
+ // left behind takes the name back, so the mark always describes the value that
134
+ // is actually in the bag. The unmark is a no-op unless a generic write happened
135
+ // on this element, which keeps the common path free of any bookkeeping.
136
+ const setDataProp = (ctx, key, value) => {
137
+ const props = writeDataProp(ctx, key, value);
138
+ unmarkClientOnlyProperty(props, key);
139
+ };
140
+ // `Prop` writes a DOM property and claims nothing about what it means: the
141
+ // value may be a client-only component property, and the name may collide with
142
+ // one an attribute builder owns. `CustomElement.define` property factories
143
+ // produce `Prop`, so a declared component property lands here too.
144
+ const setClientOnlyDataProp = (ctx, key, value) => {
145
+ const props = writeDataProp(ctx, key, value);
146
+ markClientOnlyProperty(props, key);
147
+ };
148
+ // NOTE: `h.InnerHTML` marks the value it wrote as markup the view author intends
149
+ // to be parsed as HTML, which is what lets the server serializer write it to the
150
+ // raw sink. A custom-element property or raw `Prop` named `innerHTML` marks the
151
+ // same key client-only instead. The marks are mutually exclusive, so the last
152
+ // builder owns the key even when both builders wrote exactly the same string.
153
+ const setTrustedInnerHtml = (ctx, value) => {
154
+ const props = writeDataProp(ctx, 'innerHTML', value);
155
+ markTrustedInnerHtml(props, value);
118
156
  };
119
157
  const setDataAttr = (ctx, key, value) => {
120
158
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
@@ -176,6 +214,62 @@ const updateDataOn = (ctx, on) => {
176
214
  }
177
215
  }
178
216
  };
217
+ // The values a numeric builder accepts, per property.
218
+ //
219
+ // The server writes these as attribute text and a browser parses them; the
220
+ // client assigns them through the DOM property. The two do not agree
221
+ // everywhere. Assigning a negative `maxLength` throws IndexSizeError while the
222
+ // attribute parses to -1; `size = 0` throws while the attribute falls back to
223
+ // 20; `Infinity` and `NaN` become 0 through the property and the attribute's own
224
+ // default through the parser; and past 2^31 the property conversions wrap while
225
+ // the attribute clamps.
226
+ //
227
+ // The bounds below were measured in Chromium: within them, a parsed attribute
228
+ // and a direct assignment produce the same property for every builder here.
229
+ // Outside them the two disagree or the assignment throws, so the value is
230
+ // refused where it is written rather than diverging once served.
231
+ const SIGNED_LONG_MAXIMUM = 2147483647;
232
+ const SIGNED_LONG_MINIMUM = -SIGNED_LONG_MAXIMUM - 1;
233
+ const NUMERIC_PROPERTY_RANGES = {
234
+ cols: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
235
+ colSpan: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
236
+ maxLength: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
237
+ minLength: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
238
+ rows: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
239
+ rowSpan: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
240
+ size: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
241
+ span: { minimum: 0, maximum: SIGNED_LONG_MAXIMUM },
242
+ // An ordered list may start anywhere, and negatives agree on both sides.
243
+ start: { minimum: SIGNED_LONG_MINIMUM, maximum: SIGNED_LONG_MAXIMUM },
244
+ // Any element can carry one, and the two sides agree across the whole signed
245
+ // long range. Outside it they part company in a way no value hints at: an
246
+ // out-of-range assignment wraps (2147483648 becomes -2147483648) while the
247
+ // attribute falls back to the element's default, which is 0 on a focusable
248
+ // element and -1 on any other, so the same view yields a focusable served
249
+ // element and an unreachable fresh one.
250
+ tabIndex: { minimum: SIGNED_LONG_MINIMUM, maximum: SIGNED_LONG_MAXIMUM },
251
+ };
252
+ const numericPropertyRefusal = (propName, value) => `[foldkit] ${propName} was given ${String(value)}, which a browser reads ` +
253
+ 'differently depending on whether it arrives as parsed markup or as a ' +
254
+ 'property assignment, and which can throw outright when assigned. ';
255
+ const setNumericDataProp = (ctx, propName, value) => {
256
+ const range = NUMERIC_PROPERTY_RANGES[propName];
257
+ if (range !== undefined &&
258
+ !(Number.isInteger(value) &&
259
+ value >= range.minimum &&
260
+ value <= range.maximum)) {
261
+ throw new Error(numericPropertyRefusal(propName, value) +
262
+ `Use an integer from ${String(range.minimum)} to ` +
263
+ `${String(range.maximum)}.`);
264
+ }
265
+ setDataProp(ctx, propName, value);
266
+ };
267
+ const setFiniteNumericDataProp = (ctx, propName, value) => {
268
+ if (!Number.isFinite(value)) {
269
+ throw new Error(numericPropertyRefusal(propName, value) + 'Use a finite number.');
270
+ }
271
+ setDataProp(ctx, propName, value);
272
+ };
179
273
  const updatePropsWithPostpatch = (ctx, propName, value) => {
180
274
  setDataProp(ctx, propName, value);
181
275
  ctx.getPostpatchProps().push({ propName, value });
@@ -238,7 +332,7 @@ const attributeHandlers = {
238
332
  Title: ({ value }, ctx) => setDataProp(ctx, 'title', value),
239
333
  Lang: ({ value }, ctx) => setDataProp(ctx, 'lang', value),
240
334
  Dir: ({ value }, ctx) => setDataProp(ctx, 'dir', value),
241
- Tabindex: ({ value }, ctx) => setDataProp(ctx, 'tabIndex', value),
335
+ Tabindex: ({ value }, ctx) => setNumericDataProp(ctx, 'tabIndex', value),
242
336
  Hidden: ({ value }, ctx) => setDataProp(ctx, 'hidden', value),
243
337
  Contenteditable: ({ value }, ctx) => setDataAttr(ctx, 'contenteditable', value),
244
338
  Draggable: ({ value }, ctx) => setDataProp(ctx, 'draggable', value),
@@ -536,11 +630,11 @@ const attributeHandlers = {
536
630
  Accept: ({ value }, ctx) => setDataProp(ctx, 'accept', value),
537
631
  Autocomplete: ({ value }, ctx) => setDataProp(ctx, 'autocomplete', value),
538
632
  Pattern: ({ value }, ctx) => setDataProp(ctx, 'pattern', value),
539
- Maxlength: ({ value }, ctx) => setDataProp(ctx, 'maxLength', value),
540
- Minlength: ({ value }, ctx) => setDataProp(ctx, 'minLength', value),
541
- Size: ({ value }, ctx) => setDataProp(ctx, 'size', value),
542
- Cols: ({ value }, ctx) => setDataProp(ctx, 'cols', value),
543
- Rows: ({ value }, ctx) => setDataProp(ctx, 'rows', value),
633
+ Maxlength: ({ value }, ctx) => setNumericDataProp(ctx, 'maxLength', value),
634
+ Minlength: ({ value }, ctx) => setNumericDataProp(ctx, 'minLength', value),
635
+ Size: ({ value }, ctx) => setNumericDataProp(ctx, 'size', value),
636
+ Cols: ({ value }, ctx) => setNumericDataProp(ctx, 'cols', value),
637
+ Rows: ({ value }, ctx) => setNumericDataProp(ctx, 'rows', value),
544
638
  Max: ({ value }, ctx) => setDataProp(ctx, 'max', value),
545
639
  Min: ({ value }, ctx) => setDataProp(ctx, 'min', value),
546
640
  Step: ({ value }, ctx) => setDataProp(ctx, 'step', value),
@@ -560,12 +654,12 @@ const attributeHandlers = {
560
654
  Formnovalidate: ({ value }, ctx) => setDataProp(ctx, 'formNoValidate', value),
561
655
  Formtarget: ({ value }, ctx) => setDataProp(ctx, 'formTarget', value),
562
656
  Formenctype: ({ value }, ctx) => setDataProp(ctx, 'formEnctype', value),
563
- Colspan: ({ value }, ctx) => setDataProp(ctx, 'colSpan', value),
564
- Rowspan: ({ value }, ctx) => setDataProp(ctx, 'rowSpan', value),
657
+ Colspan: ({ value }, ctx) => setNumericDataProp(ctx, 'colSpan', value),
658
+ Rowspan: ({ value }, ctx) => setNumericDataProp(ctx, 'rowSpan', value),
565
659
  Scope: ({ value }, ctx) => setDataAttr(ctx, 'scope', value),
566
660
  Headers: ({ value }, ctx) => setDataAttr(ctx, 'headers', value),
567
- Span: ({ value }, ctx) => setDataProp(ctx, 'span', value),
568
- Start: ({ value }, ctx) => setDataProp(ctx, 'start', value),
661
+ Span: ({ value }, ctx) => setNumericDataProp(ctx, 'span', value),
662
+ Start: ({ value }, ctx) => setNumericDataProp(ctx, 'start', value),
569
663
  Reversed: ({ value }, ctx) => setDataProp(ctx, 'reversed', value),
570
664
  CiteAttr: ({ value }, ctx) => setDataProp(ctx, 'cite', value),
571
665
  Datetime: ({ value }, ctx) => setDataProp(ctx, 'dateTime', value),
@@ -596,9 +690,9 @@ const attributeHandlers = {
596
690
  Poster: ({ value }, ctx) => setDataProp(ctx, 'poster', value),
597
691
  Preload: ({ value }, ctx) => setDataProp(ctx, 'preload', value),
598
692
  Playsinline: ({ value }, ctx) => setDataProp(ctx, 'playsInline', value),
599
- High: ({ value }, ctx) => setDataProp(ctx, 'high', value),
600
- Low: ({ value }, ctx) => setDataProp(ctx, 'low', value),
601
- Optimum: ({ value }, ctx) => setDataProp(ctx, 'optimum', value),
693
+ High: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'high', value),
694
+ Low: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'low', value),
695
+ Optimum: ({ value }, ctx) => setFiniteNumericDataProp(ctx, 'optimum', value),
602
696
  Usemap: ({ value }, ctx) => setDataAttr(ctx, 'usemap', value),
603
697
  Ismap: ({ value }, ctx) => setDataProp(ctx, 'isMap', value),
604
698
  Role: ({ value }, ctx) => setDataAttr(ctx, 'role', value),
@@ -650,8 +744,8 @@ const attributeHandlers = {
650
744
  AriaValuetext: ({ value }, ctx) => setDataAttr(ctx, 'aria-valuetext', value),
651
745
  Attribute: ({ key, value }, ctx) => setDataAttr(ctx, key, value),
652
746
  DataAttribute: ({ key, value }, ctx) => setDataAttr(ctx, `data-${key}`, value),
653
- Style: ({ value }, ctx) => setModuleData(ctx, 'style', value, VNodeDataMask.Style),
654
- InnerHTML: ({ value }, ctx) => setDataProp(ctx, 'innerHTML', value),
747
+ Style: ({ value }, ctx) => setModuleData(ctx, 'style', normalizedStyleProperties(value), VNodeDataMask.Style),
748
+ InnerHTML: ({ value }, ctx) => setTrustedInnerHtml(ctx, value),
655
749
  ViewBox: ({ value }, ctx) => setDataAttr(ctx, 'viewBox', value),
656
750
  Xmlns: ({ value }, ctx) => setDataAttr(ctx, 'xmlns', value),
657
751
  Fill: ({ value }, ctx) => setDataAttr(ctx, 'fill', value),
@@ -742,7 +836,7 @@ const attributeHandlers = {
742
836
  RefY: ({ value }, ctx) => setDataAttr(ctx, 'refY', value),
743
837
  Orient: ({ value }, ctx) => setDataAttr(ctx, 'orient', value),
744
838
  PreserveAspectRatio: ({ value }, ctx) => setDataAttr(ctx, 'preserveAspectRatio', value),
745
- Prop: ({ key, value }, ctx) => setDataProp(ctx, key, value),
839
+ Prop: ({ key, value }, ctx) => setClientOnlyDataProp(ctx, key, value),
746
840
  OnCustomEvent: ({ name, f: toMessage }, ctx) => updateDataOn(ctx, {
747
841
  [name]: (event) => {
748
842
  if (event instanceof CustomEvent) {
@@ -868,19 +962,58 @@ const capturedContextOrEmpty = () => {
868
962
  return Context.empty();
869
963
  }
870
964
  };
965
+ // NOTE: reasserted on `insert` as well as on `postpatch`. The props module sets
966
+ // a controlled property while the element is being created, before its children
967
+ // exist, and a `<select>`'s `value` setter has nothing to match against until
968
+ // its `<option>`s are there, so a fresh render left the select on the browser's
969
+ // own default until some later patch corrected it. The server instead marks the
970
+ // matching option, so the served page was right and the fresh one was not.
971
+ // `insert` fires once the subtree is built, which is where the two agree.
972
+ const applyControlledProps = (vnode, controlledProps) => {
973
+ if (!vnode.elm) {
974
+ return;
975
+ }
976
+ Array.forEach(controlledProps, ({ propName, value }) => {
977
+ const isOutputValue = vnode.elm instanceof Element &&
978
+ vnode.elm.localName === 'output' &&
979
+ propName === 'value';
980
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
981
+ if (isOutputValue || vnode.elm[propName] !== value) {
982
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
983
+ ;
984
+ vnode.elm[propName] = value;
985
+ }
986
+ if (vnode.elm instanceof Element) {
987
+ synchronizeControlledDefault(vnode.elm, propName, value);
988
+ }
989
+ });
990
+ };
871
991
  const attachPostpatchHook = (data, postpatchProps) => {
992
+ const existingInsert = data.hook?.insert;
872
993
  data.hook = {
873
994
  ...data.hook,
995
+ insert: vnode => {
996
+ applyControlledProps(vnode, postpatchProps);
997
+ existingInsert?.(vnode);
998
+ },
874
999
  postpatch: (_oldVnode, vnode) => {
875
- if (vnode.elm) {
876
- Array.forEach(postpatchProps, ({ propName, value }) => {
877
- /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
878
- if (vnode.elm[propName] !== value) {
879
- /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
880
- ;
881
- vnode.elm[propName] = value;
882
- }
883
- });
1000
+ applyControlledProps(vnode, postpatchProps);
1001
+ },
1002
+ };
1003
+ };
1004
+ const attachControlledContentOwnershipHook = (data) => {
1005
+ const existingPostpatch = data.hook?.postpatch;
1006
+ data.hook = {
1007
+ ...data.hook,
1008
+ postpatch: (oldVnode, vnode) => {
1009
+ existingPostpatch?.(oldVnode, vnode);
1010
+ const oldProperties = oldVnode.data?.props;
1011
+ const properties = vnode.data?.props;
1012
+ if (oldProperties !== undefined &&
1013
+ Object.hasOwn(oldProperties, 'value') &&
1014
+ (properties === undefined || !Object.hasOwn(properties, 'value')) &&
1015
+ vnode.elm instanceof Element) {
1016
+ restoreUncontrolledContent(vnode.elm, vnode);
884
1017
  }
885
1018
  },
886
1019
  };
@@ -956,14 +1089,288 @@ const copyChildrenDroppingEmpty = (children) => {
956
1089
  }
957
1090
  return next;
958
1091
  };
959
- const createElement = (tagName, attributes = [], children = []) => h(tagName, buildVNodeData(attributes), copyChildrenDroppingEmpty(children));
1092
+ // Elements whose content a controlled `value` owns. Giving one both a value and
1093
+ // trusted raw HTML asks two mechanisms to own the same text.
1094
+ const CONTROLLED_CONTENT_ELEMENTS = new Set([
1095
+ 'output',
1096
+ 'select',
1097
+ 'textarea',
1098
+ ]);
1099
+ // Elements that carry no content at all, so raw HTML written into one cannot be
1100
+ // represented in served markup. Assigning the property in a browser can still
1101
+ // build child nodes, which is a difference no server render can reproduce.
1102
+ const VOID_CONTENT_ELEMENTS = new Set([
1103
+ 'area',
1104
+ 'base',
1105
+ 'br',
1106
+ 'col',
1107
+ 'embed',
1108
+ 'hr',
1109
+ 'img',
1110
+ 'input',
1111
+ 'link',
1112
+ 'meta',
1113
+ 'param',
1114
+ 'source',
1115
+ 'track',
1116
+ 'wbr',
1117
+ ]);
1118
+ // One owner per element's content. Both `h.InnerHTML` and a client-only
1119
+ // `innerHTML` property hand the element an opaque subtree. A view that also
1120
+ // declares children or a controlled value gives the differ child vnodes for
1121
+ // nodes the property replaces, leaving those vnodes detached and making a later
1122
+ // patch unable to restore the declared children. Trusted `h.InnerHTML` also
1123
+ // disagrees with the server serializer in these combinations. Rejecting at the
1124
+ // builder makes the server render, a fresh client render, and hydration refuse
1125
+ // the same self-contradictory view.
1126
+ const assertSingleContentOwner = (tagName, data, children) => {
1127
+ const lowerTagName = tagName.toLowerCase();
1128
+ if ((lowerTagName === 'textarea' || lowerTagName === 'output') &&
1129
+ data.props?.['value'] !== undefined &&
1130
+ children.length > 0) {
1131
+ throw new Error(`[foldkit] <${lowerTagName}> was given both a controlled value and ` +
1132
+ 'children. Both own the element content, so a property assignment ' +
1133
+ 'replaces the child nodes the differ expects to patch. Keep one owner.');
1134
+ }
1135
+ const properties = data.props;
1136
+ if (properties === undefined || !Object.hasOwn(properties, 'innerHTML')) {
1137
+ return;
1138
+ }
1139
+ const innerHtmlOwner = hasTrustedInnerHtml(properties)
1140
+ ? 'h.InnerHTML'
1141
+ : 'a client-only innerHTML property';
1142
+ if (children.length > 0) {
1143
+ throw new Error(`[foldkit] <${lowerTagName}> was given both ${innerHtmlOwner} and children. ` +
1144
+ 'innerHTML owns the whole of an element\u2019s content, so the two ' +
1145
+ 'cannot both describe it: server rendering emits the raw HTML alone ' +
1146
+ 'for h.InnerHTML, while a browser property assignment replaces the ' +
1147
+ 'nodes the differ expects to patch. Keep one content owner.');
1148
+ }
1149
+ if (CONTROLLED_CONTENT_ELEMENTS.has(lowerTagName) &&
1150
+ data.props?.['value'] !== undefined) {
1151
+ throw new Error(`[foldkit] <${lowerTagName}> was given both ${innerHtmlOwner} and a ` +
1152
+ 'controlled value. Both own this element\u2019s content, and they ' +
1153
+ 'disagree once the client reasserts the value. Keep one of them.');
1154
+ }
1155
+ if (VOID_CONTENT_ELEMENTS.has(lowerTagName)) {
1156
+ throw new Error(`[foldkit] <${lowerTagName}> cannot hold content, so ${innerHtmlOwner} ` +
1157
+ 'on it ' +
1158
+ 'has no representation in served HTML. A browser can still build ' +
1159
+ 'child nodes from the property, which no server render reproduces. ' +
1160
+ 'Remove the innerHTML value.');
1161
+ }
1162
+ };
1163
+ const assertSingleStyleOwner = (data) => {
1164
+ if (data.style === undefined ||
1165
+ htmlAttributeValue(data.attrs, 'style') === undefined) {
1166
+ return;
1167
+ }
1168
+ throw new Error('[foldkit] An element was given both h.Style and a raw ' +
1169
+ "h.Attribute('style', ...). They are two owners of one declaration " +
1170
+ 'block, and CSS parsing can make one swallow or override the other. ' +
1171
+ 'Keep all inline declarations in one of them.');
1172
+ };
1173
+ // The state an element would be in once it exists, read from the tag, the raw
1174
+ // attributes, and the typed properties together. A builder on its own cannot
1175
+ // decide any of this: `h.Value` is a string on an input and a number on a
1176
+ // meter, `type="file"` refuses the value an input takes everywhere else, and a
1177
+ // raw attribute means nothing until something says which property claims the
1178
+ // same name.
1179
+ //
1180
+ // Each rule below marks a view whose server render and fresh client render
1181
+ // cannot agree. Refusing it where it is written is what makes the two, plus a
1182
+ // hydration of the first by the second, fail the same way rather than diverge
1183
+ // quietly.
1184
+ // A number both a parser and an assignment read identically: ASCII digits, one
1185
+ // optional leading minus, an optional fraction, an optional exponent. Anything
1186
+ // else parts the two: `0x10` is 16 to `Number` and invalid to the parser, a
1187
+ // leading `+` or surrounding whitespace is accepted by one and not the other,
1188
+ // and `Infinity` throws on assignment while the attribute falls back to the
1189
+ // element's default.
1190
+ const DECIMAL_NUMBER = /^-?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?$/;
1191
+ const DECIMAL_INTEGER = /^-?[0-9]+$/;
1192
+ // Properties the DOM defines as numbers on these elements and as strings
1193
+ // elsewhere, so the same builder means different things depending on where it
1194
+ // is written.
1195
+ const DOUBLE_IDL_PROPERTIES = {
1196
+ meter: new Set(['high', 'low', 'max', 'min', 'optimum', 'value']),
1197
+ progress: new Set(['max', 'value']),
1198
+ };
1199
+ const LONG_IDL_PROPERTIES = {
1200
+ li: new Set(['value']),
1201
+ };
1202
+ const assertDoubleIdlValue = (tagName, propName, value) => {
1203
+ const isRepresentable = typeof value === 'number'
1204
+ ? Number.isFinite(value)
1205
+ : typeof value === 'string' &&
1206
+ DECIMAL_NUMBER.test(value) &&
1207
+ Number.isFinite(Number(value));
1208
+ if (!isRepresentable) {
1209
+ throw new Error(`[foldkit] <${tagName}> reads ${propName} as a number, and ` +
1210
+ `${JSON.stringify(value)} is not one a browser reads the same way from ` +
1211
+ 'markup and from a property assignment. Use a finite decimal number.');
1212
+ }
1213
+ };
1214
+ const assertLongIdlValue = (tagName, propName, value) => {
1215
+ const parsed = typeof value === 'number'
1216
+ ? value
1217
+ : typeof value === 'string' && DECIMAL_INTEGER.test(value)
1218
+ ? Number(value)
1219
+ : Number.NaN;
1220
+ if (!Number.isInteger(parsed) ||
1221
+ parsed < SIGNED_LONG_MINIMUM ||
1222
+ parsed > SIGNED_LONG_MAXIMUM) {
1223
+ throw new Error(`[foldkit] <${tagName}> reads ${propName} as an integer, and ` +
1224
+ `${JSON.stringify(value)} is not one a browser reads the same way from ` +
1225
+ 'markup and from a property assignment. Use an integer from ' +
1226
+ `${String(SIGNED_LONG_MINIMUM)} to ${String(SIGNED_LONG_MAXIMUM)}.`);
1227
+ }
1228
+ };
1229
+ const effectiveInputType = (data) => {
1230
+ const typed = data.props?.['type'];
1231
+ if (typeof typed === 'string') {
1232
+ return typed.toLowerCase();
1233
+ }
1234
+ const raw = htmlAttributeValue(data.attrs, 'type');
1235
+ return typeof raw === 'string' ? raw.toLowerCase() : undefined;
1236
+ };
1237
+ const assertElementStateIsRepresentable = (tagName, data) => {
1238
+ const props = data.props;
1239
+ if (props === undefined) {
1240
+ return;
1241
+ }
1242
+ const lowerTagName = tagName.toLowerCase();
1243
+ const doubleIdl = DOUBLE_IDL_PROPERTIES[lowerTagName];
1244
+ const longIdl = LONG_IDL_PROPERTIES[lowerTagName];
1245
+ for (const propName of Object.keys(props)) {
1246
+ const value = props[propName];
1247
+ if (value === undefined || propName === 'innerHTML') {
1248
+ continue;
1249
+ }
1250
+ if (doubleIdl?.has(propName) === true) {
1251
+ assertDoubleIdlValue(lowerTagName, propName, value);
1252
+ }
1253
+ if (longIdl?.has(propName) === true) {
1254
+ assertLongIdlValue(lowerTagName, propName, value);
1255
+ }
1256
+ if (lowerTagName === 'input' && propName === 'size' && value === 0) {
1257
+ throw new Error(numericPropertyRefusal(propName, value) +
1258
+ `Use an integer from 1 to ${String(SIGNED_LONG_MAXIMUM)} on an input.`);
1259
+ }
1260
+ // A raw attribute and a typed builder that name the same attribute are two
1261
+ // owners of one piece of state, and their served form has no source
1262
+ // spelling. `h.Attribute('checked', '')` with `h.Checked(false)` serves no
1263
+ // attribute at all, so the served element has `defaultChecked` false while
1264
+ // a fresh render parses the attribute and has it true: `form.reset()`,
1265
+ // `:default`, and an attribute selector then read the two pages
1266
+ // differently. A client-only property carries no such claim, so a
1267
+ // `CustomElement.define` property never conflicts with an attribute.
1268
+ const attributeName = reflectedAttributeName(propName);
1269
+ if (attributeName !== undefined &&
1270
+ !isClientOnlyProperty(props, propName) &&
1271
+ htmlAttributeValue(data.attrs, attributeName) !== undefined) {
1272
+ throw new Error(`[foldkit] <${lowerTagName}> was given both a typed ${propName} and a ` +
1273
+ `raw h.Attribute('${attributeName}', ...). They are two owners of ` +
1274
+ 'one attribute, and the state they describe together has no spelling ' +
1275
+ 'in served HTML: the property decides what is served while a fresh ' +
1276
+ 'render parses the attribute as the default too. Keep one of them.');
1277
+ }
1278
+ }
1279
+ if (lowerTagName === 'input' && effectiveInputType(data) === 'file') {
1280
+ const value = props['value'];
1281
+ if (typeof value === 'string' && value !== '') {
1282
+ throw new Error('[foldkit] <input type="file"> was given a value. A file input takes ' +
1283
+ 'no value from markup or from the Model: the served attribute is ' +
1284
+ 'ignored and assigning the property throws InvalidStateError, so the ' +
1285
+ 'view crashes on a fresh render and on hydration. Drop the value.');
1286
+ }
1287
+ }
1288
+ };
1289
+ // NOTE: A typed property on an HTML element whose native interface does not own it
1290
+ // has always been a client-side expando. For example, RadioGroup deliberately
1291
+ // includes `h.Type('button')` in an attribute bundle that consumers may spread
1292
+ // onto a button, div, or span. Mark the wrong-element case as client-only so
1293
+ // server rendering omits it and hydration applies the same expando a fresh
1294
+ // render does, without turning a pre-existing client pattern into live markup.
1295
+ const markUnreflectedHtmlPropertiesClientOnly = (tagName, data) => {
1296
+ const props = data.props;
1297
+ if (props === undefined) {
1298
+ return;
1299
+ }
1300
+ for (const propName of Object.keys(props)) {
1301
+ if (reflectedAttributeName(propName) !== undefined &&
1302
+ !isHtmlPropertyRepresentable(tagName, propName)) {
1303
+ markClientOnlyProperty(props, propName);
1304
+ }
1305
+ }
1306
+ };
1307
+ // The same question in SVG and MathML, where the answer is decided by the
1308
+ // namespace rather than by the tag. A foreign element has none of the HTML
1309
+ // interface members a typed builder writes, apart from the three measured to
1310
+ // reflect there. Assigning `href` on an SVG `<a>` throws, since bundled code is
1311
+ // strict and `SVGAElement.href` is readonly; assigning `title` sets an expando
1312
+ // no attribute ever sees. The serializer would have written an attribute for
1313
+ // both. Raw `h.Attribute` is the mechanism SVG and MathML use, and it behaves
1314
+ // identically on both sides.
1315
+ //
1316
+ // The namespace is only known once the `svg` or `math` element that introduces
1317
+ // it is built, because that is when it propagates down. The walk stops where
1318
+ // the namespace stops, so an HTML integration point such as `<foreignObject>`
1319
+ // keeps ordinary HTML rules for its content.
1320
+ const assertForeignPropertiesAreRepresentable = (node) => {
1321
+ const data = node.data;
1322
+ if (data === undefined || data.ns === undefined) {
1323
+ return;
1324
+ }
1325
+ const props = data.props;
1326
+ if (props !== undefined) {
1327
+ for (const propName of Object.keys(props)) {
1328
+ if (props[propName] === undefined ||
1329
+ propName === 'innerHTML' ||
1330
+ FOREIGN_REPRESENTABLE_PROPERTIES.has(propName) ||
1331
+ reflectedAttributeName(propName) === undefined) {
1332
+ continue;
1333
+ }
1334
+ throw new Error(`[foldkit] <${tagNameFromSelector(node.sel ?? '')}> is in the SVG or ` +
1335
+ `MathML namespace, where ${propName} is not a property the element ` +
1336
+ 'has. Assigning it on the client sets a value no attribute reflects, ' +
1337
+ 'or throws outright, while server rendering writes the attribute, so ' +
1338
+ `the two disagree. Write it as h.Attribute('${propName}', ...), ` +
1339
+ 'which foreign content reads the same way on both sides.');
1340
+ }
1341
+ }
1342
+ for (const child of node.children ?? []) {
1343
+ if (typeof child !== 'string') {
1344
+ assertForeignPropertiesAreRepresentable(child);
1345
+ }
1346
+ }
1347
+ };
1348
+ const buildElement = (tagName, data, children) => {
1349
+ const copiedChildren = copyChildrenDroppingEmpty(children);
1350
+ markUnreflectedHtmlPropertiesClientOnly(tagName, data);
1351
+ assertSingleContentOwner(tagName, data, copiedChildren);
1352
+ assertSingleStyleOwner(data);
1353
+ if (data.style !== undefined) {
1354
+ assertStyleIsRepresentable(data.style);
1355
+ }
1356
+ assertElementStateIsRepresentable(tagName, data);
1357
+ if (tagName.toLowerCase() === 'select' ||
1358
+ tagName.toLowerCase() === 'textarea' ||
1359
+ tagName.toLowerCase() === 'output') {
1360
+ attachControlledContentOwnershipHook(data);
1361
+ }
1362
+ const built = h(tagName, data, copiedChildren);
1363
+ assertForeignPropertiesAreRepresentable(built);
1364
+ return built;
1365
+ };
1366
+ const createElement = (tagName, attributes = [], children = []) => buildElement(tagName, buildVNodeData(attributes), children);
960
1367
  const element = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
961
1368
  export const customElement = () => (tagName) => (attributes = [], children = []) => createElement(tagName, attributes, children);
962
1369
  const voidElement = () => (tagName) => (attributes = []) => createElement(tagName, attributes, []);
963
1370
  const keyed = () => (tagName) => (key, attributes = [], children = []) => {
964
1371
  const data = buildVNodeData(attributes);
965
1372
  data.key = key;
966
- return h(tagName, data, copyChildrenDroppingEmpty(children));
1373
+ return buildElement(tagName, data, children);
967
1374
  };
968
1375
  const htmlElements = () => {
969
1376
  const el = element();
package/dist/hydrate.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { VNode } from './snabbdom/index.js';
2
2
  export declare const __elementSignature: (element: Element, vnode: VNode) => string;
3
- export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen?: Set<object>) => VNode;
3
+ export declare const __hydrateVNode: (hydrationRoot: Element, nextVNode: VNode | null, seen: Set<object> | undefined, buildId: string) => VNode;
4
4
  //# sourceMappingURL=hydrate.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAwPhD,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,EAAE,OAAO,KAAK,KAAG,MA4BnE,CAAA;AAmsBD,eAAO,MAAM,cAAc,GACzB,eAAe,OAAO,EACtB,WAAW,KAAK,GAAG,IAAI,EACvB,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,EAC7B,SAAS,MAAM,KACd,KAwDF,CAAA"}