foldkit 0.146.0 → 0.148.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 (91) hide show
  1. package/README.md +5 -2
  2. package/dist/buildToken.d.ts +3 -0
  3. package/dist/buildToken.d.ts.map +1 -0
  4. package/dist/buildToken.js +21 -0
  5. package/dist/controlledDomState.d.ts +25 -0
  6. package/dist/controlledDomState.d.ts.map +1 -0
  7. package/dist/controlledDomState.js +240 -0
  8. package/dist/cssStyleProperties.d.ts +6 -0
  9. package/dist/cssStyleProperties.d.ts.map +1 -0
  10. package/dist/cssStyleProperties.js +91 -0
  11. package/dist/customElement/index.d.ts.map +1 -1
  12. package/dist/customElement/index.js +23 -0
  13. package/dist/domReflection.d.ts +73 -0
  14. package/dist/domReflection.d.ts.map +1 -0
  15. package/dist/domReflection.js +557 -0
  16. package/dist/experimental/index.d.ts +1 -0
  17. package/dist/experimental/index.d.ts.map +1 -1
  18. package/dist/experimental/index.js +1 -0
  19. package/dist/experimental/machine/machine.d.ts +22 -3
  20. package/dist/experimental/machine/machine.d.ts.map +1 -1
  21. package/dist/experimental/machine/machine.js +8 -0
  22. package/dist/experimental/server/entry.d.ts +73 -0
  23. package/dist/experimental/server/entry.d.ts.map +1 -0
  24. package/dist/experimental/server/entry.js +41 -0
  25. package/dist/experimental/server/host.d.ts +136 -0
  26. package/dist/experimental/server/host.d.ts.map +1 -0
  27. package/dist/experimental/server/host.js +359 -0
  28. package/dist/experimental/server/index.d.ts +5 -0
  29. package/dist/experimental/server/index.d.ts.map +1 -0
  30. package/dist/experimental/server/index.js +4 -0
  31. package/dist/experimental/server/public.d.ts +3 -0
  32. package/dist/experimental/server/public.d.ts.map +1 -0
  33. package/dist/experimental/server/public.js +1 -0
  34. package/dist/experimental/server/serialize.d.ts +43 -0
  35. package/dist/experimental/server/serialize.d.ts.map +1 -0
  36. package/dist/experimental/server/serialize.js +786 -0
  37. package/dist/experimental/server/server.d.ts +242 -0
  38. package/dist/experimental/server/server.d.ts.map +1 -0
  39. package/dist/experimental/server/server.js +946 -0
  40. package/dist/experimental/server/template.d.ts +53 -0
  41. package/dist/experimental/server/template.d.ts.map +1 -0
  42. package/dist/experimental/server/template.js +617 -0
  43. package/dist/html/index.d.ts +5 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +474 -33
  46. package/dist/hydrate.d.ts +4 -0
  47. package/dist/hydrate.d.ts.map +1 -0
  48. package/dist/hydrate.js +850 -0
  49. package/dist/hydrationMarker.d.ts +10 -0
  50. package/dist/hydrationMarker.d.ts.map +1 -0
  51. package/dist/hydrationMarker.js +9 -0
  52. package/dist/hydrationMarkers.d.ts +15 -0
  53. package/dist/hydrationMarkers.d.ts.map +1 -0
  54. package/dist/hydrationMarkers.js +70 -0
  55. package/dist/nativeInnerHtml.d.ts +13 -0
  56. package/dist/nativeInnerHtml.d.ts.map +1 -0
  57. package/dist/nativeInnerHtml.js +30 -0
  58. package/dist/propertyProvenance.d.ts +33 -0
  59. package/dist/propertyProvenance.d.ts.map +1 -0
  60. package/dist/propertyProvenance.js +78 -0
  61. package/dist/propsModule.d.ts.map +1 -1
  62. package/dist/propsModule.js +149 -15
  63. package/dist/runtime/public.d.ts +2 -2
  64. package/dist/runtime/public.d.ts.map +1 -1
  65. package/dist/runtime/public.js +1 -1
  66. package/dist/runtime/runtime.d.ts +99 -36
  67. package/dist/runtime/runtime.d.ts.map +1 -1
  68. package/dist/runtime/runtime.js +412 -71
  69. package/dist/snabbdom/attributes.d.ts.map +1 -1
  70. package/dist/snabbdom/attributes.js +65 -37
  71. package/dist/snabbdom/h.d.ts +1 -0
  72. package/dist/snabbdom/h.d.ts.map +1 -1
  73. package/dist/snabbdom/h.js +85 -4
  74. package/dist/snabbdom/style.d.ts.map +1 -1
  75. package/dist/snabbdom/style.js +53 -34
  76. package/dist/snabbdom/tovnode.d.ts.map +1 -1
  77. package/dist/snabbdom/tovnode.js +5 -1
  78. package/dist/tagName.d.ts +6 -0
  79. package/dist/tagName.d.ts.map +1 -0
  80. package/dist/tagName.js +11 -0
  81. package/dist/test/apps/attributes.d.ts +1 -0
  82. package/dist/test/apps/attributes.d.ts.map +1 -1
  83. package/dist/test/apps/attributes.js +8 -1
  84. package/dist/test/apps/login.js +1 -1
  85. package/dist/test/matchers.d.ts.map +1 -1
  86. package/dist/test/matchers.js +2 -1
  87. package/dist/test/scene.d.ts.map +1 -1
  88. package/dist/test/scene.js +2 -1
  89. package/dist/vdom.d.ts.map +1 -1
  90. package/dist/vdom.js +25 -1
  91. package/package.json +9 -2
@@ -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';
@@ -0,0 +1,15 @@
1
+ export declare const HYDRATION_KEY_ATTRIBUTE = "data-foldkit-key";
2
+ export declare const HYDRATION_IDENTITY_ATTRIBUTE = "data-foldkit-identity";
3
+ /** The marker value for a vnode key, or `undefined` for a key type the
4
+ * hydration handoff does not support (a symbol, which the server refuses to
5
+ * render and hydration therefore never adopts).
6
+ *
7
+ * @internal
8
+ */
9
+ export declare const hydrationKeyMarker: (key: PropertyKey) => string | undefined;
10
+ /** The marker value for a compiler-assigned view identity.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare const hydrationIdentityMarker: (identity: string) => string;
15
+ //# sourceMappingURL=hydrationMarkers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrationMarkers.d.ts","sourceRoot":"","sources":["../src/hydrationMarkers.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,uBAAuB,qBAAqB,CAAA;AACzD,eAAO,MAAM,4BAA4B,0BAA0B,CAAA;AAkCnE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,WAAW,KAAG,MAAM,GAAG,SAQ9D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU,MAAM,KAAG,MAClC,CAAA"}
@@ -0,0 +1,70 @@
1
+ // Hydration markers the serializer stamps onto a keyed or identity-bearing
2
+ // element, and hydration reads then strips. The server HTML does not otherwise
3
+ // encode a vnode's `key` or `identity`, so a reordered or stale keyed list would
4
+ // be adopted positionally: the client's first row would take over the DOM node
5
+ // (and the user state on it) of a different logical row. Serializing the markers
6
+ // lets hydration verify it is adopting the same logical entity by key and
7
+ // identity, not just by tag and position, and rebuild when it is not.
8
+ //
9
+ // A marker carries a digest of the key, never the key itself. Application keys
10
+ // are database ids, account identifiers, and email addresses the view otherwise
11
+ // never renders, and a compiler identity spells out a source path and function
12
+ // name; neither belongs in public HTML. A digest is a one-way comparison token:
13
+ // it is enough for hydration, which only ever asks whether two markers are
14
+ // equal, and it does not read back to the value it was computed from. It is not
15
+ // a secret, since a reader who guesses a candidate key can hash it and compare,
16
+ // so it is sized to make an accidental collision between two distinct keys
17
+ // vanishingly unlikely rather than to withstand a guessing attack.
18
+ //
19
+ // Markers are part of the hydration handoff, so they are emitted only for a
20
+ // hydratable render. Non-hydratable output carries no key or identity channel
21
+ // at all.
22
+ export const HYDRATION_KEY_ATTRIBUTE = 'data-foldkit-key';
23
+ export const HYDRATION_IDENTITY_ATTRIBUTE = 'data-foldkit-identity';
24
+ // FNV-1a, run as two 32-bit lanes from different offset bases and concatenated
25
+ // to 64 bits. The lanes are plain integer arithmetic over the string's code
26
+ // units, so the server and the browser compute the same digest for the same
27
+ // input with no platform dependency and no async crypto API.
28
+ const FNV_PRIME = 16777619;
29
+ const FNV_OFFSET_BASIS = 2166136261;
30
+ const SECOND_LANE_OFFSET_BASIS = 1099511628;
31
+ const digest = (value) => {
32
+ let lowLane = FNV_OFFSET_BASIS;
33
+ let highLane = SECOND_LANE_OFFSET_BASIS;
34
+ for (let index = 0; index < value.length; index += 1) {
35
+ const codeUnit = value.charCodeAt(index);
36
+ const lowByte = codeUnit & 0xff;
37
+ const highByte = codeUnit >>> 8;
38
+ lowLane = Math.imul(lowLane ^ lowByte, FNV_PRIME);
39
+ lowLane = Math.imul(lowLane ^ highByte, FNV_PRIME);
40
+ highLane = Math.imul(highLane ^ highByte, FNV_PRIME);
41
+ highLane = Math.imul(highLane ^ lowByte, FNV_PRIME);
42
+ }
43
+ return ((lowLane >>> 0).toString(16).padStart(8, '0') +
44
+ (highLane >>> 0).toString(16).padStart(8, '0'));
45
+ };
46
+ // Each digest is taken over a type-tagged string, which keeps the supported key
47
+ // types apart. The runtime compares keys with `===`, so the number 1 and the
48
+ // string '1' are different keys and must digest differently; untagged, both
49
+ // would reach the digest as "1" and a server-rendered numeric key would adopt a
50
+ // client string key's node, and the state on it.
51
+ /** The marker value for a vnode key, or `undefined` for a key type the
52
+ * hydration handoff does not support (a symbol, which the server refuses to
53
+ * render and hydration therefore never adopts).
54
+ *
55
+ * @internal
56
+ */
57
+ export const hydrationKeyMarker = (key) => {
58
+ if (typeof key === 'string') {
59
+ return digest(`s:${key}`);
60
+ }
61
+ if (typeof key === 'number') {
62
+ return digest(`n:${key}`);
63
+ }
64
+ return undefined;
65
+ };
66
+ /** The marker value for a compiler-assigned view identity.
67
+ *
68
+ * @internal
69
+ */
70
+ export const hydrationIdentityMarker = (identity) => digest(`i:${identity}`);
@@ -0,0 +1,13 @@
1
+ /** Reads the platform's `Element.innerHTML`, bypassing a same-named property
2
+ * an autonomous custom element defines.
3
+ *
4
+ * @internal
5
+ */
6
+ export declare const readNativeInnerHtml: (element: Element) => string;
7
+ /** Writes the platform's `Element.innerHTML`, bypassing a same-named property
8
+ * an autonomous custom element defines.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const writeNativeInnerHtml: (element: Element, value: string) => void;
13
+ //# sourceMappingURL=nativeInnerHtml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeInnerHtml.d.ts","sourceRoot":"","sources":["../src/nativeInnerHtml.ts"],"names":[],"mappings":"AAeA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAS,OAAO,KAAG,MAGtD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,OAAO,EAAE,OAAO,MAAM,KAAG,IAEtE,CAAA"}
@@ -0,0 +1,30 @@
1
+ const innerHtmlDescriptor = (element) => {
2
+ const elementConstructor = element.ownerDocument.defaultView?.Element ?? globalThis.Element;
3
+ const prototype = elementConstructor.prototype;
4
+ let current = prototype;
5
+ while (current !== null) {
6
+ const descriptor = Object.getOwnPropertyDescriptor(current, 'innerHTML');
7
+ if (descriptor?.get !== undefined && descriptor.set !== undefined) {
8
+ return descriptor;
9
+ }
10
+ current = Object.getPrototypeOf(current);
11
+ }
12
+ throw new Error('[foldkit] The browser does not expose Element.innerHTML.');
13
+ };
14
+ /** Reads the platform's `Element.innerHTML`, bypassing a same-named property
15
+ * an autonomous custom element defines.
16
+ *
17
+ * @internal
18
+ */
19
+ export const readNativeInnerHtml = (element) => {
20
+ const value = innerHtmlDescriptor(element).get?.call(element);
21
+ return typeof value === 'string' ? value : '';
22
+ };
23
+ /** Writes the platform's `Element.innerHTML`, bypassing a same-named property
24
+ * an autonomous custom element defines.
25
+ *
26
+ * @internal
27
+ */
28
+ export const writeNativeInnerHtml = (element, value) => {
29
+ innerHtmlDescriptor(element).set?.call(element, value);
30
+ };
@@ -0,0 +1,33 @@
1
+ /** Records `value` as markup `h.InnerHTML` wrote. The value itself is the mark,
2
+ * so a later generic write to `innerHTML` no longer matches and the markup it
3
+ * wrote is never treated as trusted.
4
+ *
5
+ * @internal
6
+ */
7
+ export declare const markTrustedInnerHtml: (props: Record<string, unknown>, value: string) => void;
8
+ /** Records `name` as a property a builder wrote without claiming it is markup:
9
+ * an internal `Prop`, which is also what a `CustomElement.define` property
10
+ * factory produces.
11
+ *
12
+ * @internal
13
+ */
14
+ export declare const markClientOnlyProperty: (props: Record<string, unknown>, name: string) => void;
15
+ /** Drops the client-only mark from `name`, for a typed attribute builder
16
+ * overwriting a property a generic write left behind.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare const unmarkClientOnlyProperty: (props: Record<string, unknown>, name: string) => void;
21
+ /** Whether `props.innerHTML` is the markup `h.InnerHTML` wrote, rather than an
22
+ * arbitrary property that happens to carry that name.
23
+ *
24
+ * @internal
25
+ */
26
+ export declare const hasTrustedInnerHtml: (props: Readonly<Record<string, unknown>> | undefined) => boolean;
27
+ /** Whether `name` is a client-only DOM property rather than the HTML attribute
28
+ * it is named after.
29
+ *
30
+ * @internal
31
+ */
32
+ export declare const isClientOnlyProperty: (props: Readonly<Record<string, unknown>> | undefined, name: string) => boolean;
33
+ //# sourceMappingURL=propertyProvenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"propertyProvenance.d.ts","sourceRoot":"","sources":["../src/propertyProvenance.ts"],"names":[],"mappings":"AAgCA;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,MAAM,KACZ,IAIF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,MAAM,KACX,IAOF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,MAAM,MAAM,KACX,IAEF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,KACnD,OASF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,EACpD,MAAM,MAAM,KACX,OAKF,CAAA"}
@@ -0,0 +1,78 @@
1
+ // What the server serializer may assume about an entry in a vnode's
2
+ // `data.props`. A DOM property name alone does not say what the property means:
3
+ // `innerHTML` is markup the view author intends to be parsed when `h.InnerHTML`
4
+ // wrote it and an arbitrary client-only value when anything else did, and `id`
5
+ // on a custom element is a reflected HTML attribute when `h.Id` wrote it and a
6
+ // component-private value when `CustomElement.define` did.
7
+ //
8
+ // Both marks belong to the last write, not to the props bag, which is what stops
9
+ // a value the serializer trusts from standing in for one written after it. The
10
+ // serializer always emits the value that is in `props`, and the marks always
11
+ // describe the builder that put it there.
12
+ //
13
+ // The marks live under symbol keys, so `Object.keys` never surfaces them, the
14
+ // props module never applies them to the DOM, the attribute serializer never
15
+ // emits them, and no string-keyed property can forge them. Object spread copies
16
+ // own enumerable symbol keys, so a copied props bag keeps its marks.
17
+ const TRUSTED_INNER_HTML = Symbol('foldkit/trusted-inner-html');
18
+ const CLIENT_ONLY_PROPERTIES = Symbol('foldkit/client-only-properties');
19
+ const markedProps = (props) =>
20
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
21
+ props;
22
+ /** Records `value` as markup `h.InnerHTML` wrote. The value itself is the mark,
23
+ * so a later generic write to `innerHTML` no longer matches and the markup it
24
+ * wrote is never treated as trusted.
25
+ *
26
+ * @internal
27
+ */
28
+ export const markTrustedInnerHtml = (props, value) => {
29
+ const marked = markedProps(props);
30
+ marked[TRUSTED_INNER_HTML] = value;
31
+ marked[CLIENT_ONLY_PROPERTIES]?.delete('innerHTML');
32
+ };
33
+ /** Records `name` as a property a builder wrote without claiming it is markup:
34
+ * an internal `Prop`, which is also what a `CustomElement.define` property
35
+ * factory produces.
36
+ *
37
+ * @internal
38
+ */
39
+ export const markClientOnlyProperty = (props, name) => {
40
+ const marked = markedProps(props);
41
+ const names = (marked[CLIENT_ONLY_PROPERTIES] ??= new Set());
42
+ names.add(name);
43
+ if (name === 'innerHTML') {
44
+ Reflect.deleteProperty(marked, TRUSTED_INNER_HTML);
45
+ }
46
+ };
47
+ /** Drops the client-only mark from `name`, for a typed attribute builder
48
+ * overwriting a property a generic write left behind.
49
+ *
50
+ * @internal
51
+ */
52
+ export const unmarkClientOnlyProperty = (props, name) => {
53
+ markedProps(props)[CLIENT_ONLY_PROPERTIES]?.delete(name);
54
+ };
55
+ /** Whether `props.innerHTML` is the markup `h.InnerHTML` wrote, rather than an
56
+ * arbitrary property that happens to carry that name.
57
+ *
58
+ * @internal
59
+ */
60
+ export const hasTrustedInnerHtml = (props) => {
61
+ if (props === undefined) {
62
+ return false;
63
+ }
64
+ const marked = markedProps(props);
65
+ const innerHtml = marked['innerHTML'];
66
+ return (typeof innerHtml === 'string' && marked[TRUSTED_INNER_HTML] === innerHtml);
67
+ };
68
+ /** Whether `name` is a client-only DOM property rather than the HTML attribute
69
+ * it is named after.
70
+ *
71
+ * @internal
72
+ */
73
+ export const isClientOnlyProperty = (props, name) => {
74
+ if (props === undefined) {
75
+ return false;
76
+ }
77
+ return markedProps(props)[CLIENT_ONLY_PROPERTIES]?.has(name) === true;
78
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"propsModule.d.ts","sourceRoot":"","sources":["../src/propsModule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AA0DjD,eAAO,MAAM,WAAW,EAAE,MAIzB,CAAA"}
1
+ {"version":3,"file":"propsModule.d.ts","sourceRoot":"","sources":["../src/propsModule.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AA0NjD,eAAO,MAAM,WAAW,EAAE,MAIzB,CAAA"}
@@ -1,8 +1,64 @@
1
+ import { clearControlledDefault, restoreControlledStateFromRawAttribute, } from './controlledDomState.js';
2
+ import { GLOBAL_ATTRIBUTE_PROPERTIES, HTML_NAMESPACE, htmlAttributeValue, reflectedAttributeName, } from './domReflection.js';
3
+ import { writeNativeInnerHtml } from './nativeInnerHtml.js';
4
+ import { hasTrustedInnerHtml, isClientOnlyProperty, } from './propertyProvenance.js';
1
5
  import { VNodeDataMask } from './snabbdom/vnode.js';
2
- /** A custom props module that extends snabbdom's built-in propsModule with
3
- * proper cleanup of removed properties.
6
+ const writeProperty = (element, name, value) => {
7
+ if (name === '__proto__') {
8
+ Object.defineProperty(element, name, {
9
+ configurable: true,
10
+ enumerable: true,
11
+ value,
12
+ writable: true,
13
+ });
14
+ }
15
+ else {
16
+ if (!Reflect.set(element, name, value)) {
17
+ throw new TypeError(`[foldkit] Cannot assign DOM property ${name}.`);
18
+ }
19
+ }
20
+ };
21
+ const isAutonomousCustomElement = (element) => (element.namespaceURI === null || element.namespaceURI === HTML_NAMESPACE) &&
22
+ element.localName.includes('-');
23
+ const isNativeCustomElementAttribute = (element, properties, name) => isAutonomousCustomElement(element) &&
24
+ GLOBAL_ATTRIBUTE_PROPERTIES.has(name) &&
25
+ !isClientOnlyProperty(properties, name);
26
+ const writeAttribute = (element, name, value) => {
27
+ const attributeName = reflectedAttributeName(name);
28
+ if (attributeName === undefined) {
29
+ return;
30
+ }
31
+ if (name === 'draggable') {
32
+ element.setAttribute(attributeName, String(value));
33
+ }
34
+ else if (value === true) {
35
+ element.setAttribute(attributeName, '');
36
+ }
37
+ else if (value === false) {
38
+ element.removeAttribute(attributeName);
39
+ }
40
+ else {
41
+ element.setAttribute(attributeName, String(value));
42
+ }
43
+ };
44
+ const resetProperty = (element, name, value) => {
45
+ if (name === '__proto__') {
46
+ Reflect.deleteProperty(element, name);
47
+ }
48
+ else if (typeof value === 'boolean') {
49
+ writeProperty(element, name, false);
50
+ }
51
+ else if (typeof value === 'string') {
52
+ writeProperty(element, name, '');
53
+ }
54
+ else if (typeof value === 'number') {
55
+ writeProperty(element, name, 0);
56
+ }
57
+ };
58
+ /** A props module that extends Snabbdom's built-in module with cleanup of
59
+ * removed properties and explicit controlled-state ownership transitions.
4
60
  *
5
- * Snabbdom's propsModule only iterates over _new_ props it never resets
61
+ * Snabbdom's propsModule only iterates over new props. It never resets
6
62
  * old props that disappeared between renders. This means `elm.disabled = true`
7
63
  * persists even after `Disabled(true)` is removed from the attribute array.
8
64
  * Since a disabled button swallows click events at the browser level, an
@@ -10,7 +66,10 @@ import { VNodeDataMask } from './snabbdom/vnode.js';
10
66
  *
11
67
  * This module adds a second loop (mirroring what snabbdom's attributesModule
12
68
  * already does) that resets removed props to type-appropriate defaults:
13
- * booleans false, strings '', numbers 0. */
69
+ * booleans become false, strings become empty, and numbers become zero. For
70
+ * reflected form and media state it also keeps the parsed default state in
71
+ * step with the controlled value, then restores raw-attribute ownership when
72
+ * the typed property leaves. */
14
73
  function updateProps(oldVnode, vnode) {
15
74
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
75
  const elm = vnode.elm;
@@ -26,25 +85,100 @@ function updateProps(oldVnode, vnode) {
26
85
  }
27
86
  oldProps = oldProps ?? {};
28
87
  props = props ?? {};
29
- for (const key in props) {
88
+ if (elm instanceof Element) {
89
+ for (const key of Object.keys(oldProps)) {
90
+ if (key === 'innerHTML' &&
91
+ Object.hasOwn(props, key) &&
92
+ hasTrustedInnerHtml(oldProps) !== hasTrustedInnerHtml(props)) {
93
+ if (hasTrustedInnerHtml(oldProps)) {
94
+ writeNativeInnerHtml(elm, '');
95
+ }
96
+ else {
97
+ resetProperty(elm, key, oldProps[key]);
98
+ }
99
+ }
100
+ if (Object.hasOwn(props, key) &&
101
+ !isClientOnlyProperty(oldProps, key) &&
102
+ isClientOnlyProperty(props, key)) {
103
+ clearControlledDefault(elm, key);
104
+ if (isNativeCustomElementAttribute(elm, oldProps, key)) {
105
+ const attributeName = reflectedAttributeName(key);
106
+ if (attributeName !== undefined) {
107
+ elm.removeAttribute(attributeName);
108
+ }
109
+ }
110
+ }
111
+ else if (Object.hasOwn(props, key) &&
112
+ isClientOnlyProperty(oldProps, key) &&
113
+ !isClientOnlyProperty(props, key) &&
114
+ isNativeCustomElementAttribute(elm, props, key)) {
115
+ resetProperty(elm, key, oldProps[key]);
116
+ }
117
+ }
118
+ }
119
+ for (const key of Object.keys(props)) {
30
120
  const cur = props[key];
31
121
  const old = oldProps[key];
32
- if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
33
- elm[key] = cur;
122
+ const isOwnershipChanged = isClientOnlyProperty(oldProps, key) !== isClientOnlyProperty(props, key);
123
+ if ((old !== cur || isOwnershipChanged) &&
124
+ (key !== 'value' || elm[key] !== cur)) {
125
+ if (key === 'innerHTML' &&
126
+ elm instanceof Element &&
127
+ hasTrustedInnerHtml(props)) {
128
+ writeNativeInnerHtml(elm, String(cur));
129
+ }
130
+ else if (elm instanceof Element &&
131
+ isNativeCustomElementAttribute(elm, props, key)) {
132
+ writeAttribute(elm, key, cur);
133
+ }
134
+ else {
135
+ writeProperty(elm, key, cur);
136
+ }
34
137
  }
35
138
  }
36
- for (const key in oldProps) {
37
- if (!(key in props)) {
139
+ for (const key of Object.keys(oldProps)) {
140
+ if (!Object.hasOwn(props, key)) {
38
141
  const old = oldProps[key];
39
- if (typeof old === 'boolean') {
40
- elm[key] = false;
142
+ if (key === 'innerHTML' &&
143
+ elm instanceof Element &&
144
+ hasTrustedInnerHtml(oldProps)) {
145
+ writeNativeInnerHtml(elm, '');
146
+ continue;
147
+ }
148
+ if (elm instanceof Element &&
149
+ isNativeCustomElementAttribute(elm, oldProps, key)) {
150
+ const attributeName = reflectedAttributeName(key);
151
+ if (attributeName !== undefined) {
152
+ elm.removeAttribute(attributeName);
153
+ }
154
+ continue;
155
+ }
156
+ if (elm instanceof Element &&
157
+ key === 'value' &&
158
+ (elm.localName === 'select' ||
159
+ elm.localName === 'textarea' ||
160
+ elm.localName === 'output')) {
161
+ if (!Object.hasOwn(props, 'innerHTML')) {
162
+ clearControlledDefault(elm, key);
163
+ }
164
+ continue;
41
165
  }
42
- else if (typeof old === 'string') {
43
- elm[key] = '';
166
+ const attributeName = reflectedAttributeName(key);
167
+ if (attributeName !== undefined &&
168
+ !isClientOnlyProperty(oldProps, key) &&
169
+ elm instanceof Element) {
170
+ if (htmlAttributeValue(vnode.data?.attrs, attributeName) !== undefined) {
171
+ restoreControlledStateFromRawAttribute(elm, key);
172
+ continue;
173
+ }
174
+ clearControlledDefault(elm, key);
175
+ elm.removeAttribute(attributeName);
176
+ continue;
44
177
  }
45
- else if (typeof old === 'number') {
46
- elm[key] = 0;
178
+ if (elm instanceof Element) {
179
+ clearControlledDefault(elm, key);
47
180
  }
181
+ resetProperty(elm, key, old);
48
182
  }
49
183
  }
50
184
  }
@@ -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, HydrateOptions, 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,cAAc,EACd,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';