foldkit 0.148.0 → 0.148.2-canary.34a250d515a4

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 (104) hide show
  1. package/README.md +25 -32
  2. package/dist/controlledDomState.d.ts.map +1 -1
  3. package/dist/controlledDomState.js +5 -1
  4. package/dist/experimental/server/serialize.d.ts +4 -4
  5. package/dist/experimental/server/serialize.d.ts.map +1 -1
  6. package/dist/experimental/server/serialize.js +25 -9
  7. package/dist/experimental/server/server.d.ts +10 -7
  8. package/dist/experimental/server/server.d.ts.map +1 -1
  9. package/dist/experimental/server/server.js +5 -4
  10. package/dist/experimental/server/template.d.ts +5 -1
  11. package/dist/experimental/server/template.d.ts.map +1 -1
  12. package/dist/experimental/server/template.js +16 -4
  13. package/dist/hydrate.d.ts.map +1 -1
  14. package/dist/hydrate.js +56 -8
  15. package/dist/hydrationMarker.d.ts +4 -3
  16. package/dist/hydrationMarker.d.ts.map +1 -1
  17. package/dist/hydrationMarker.js +4 -3
  18. package/dist/hydrationMarkers.d.ts +3 -3
  19. package/dist/hydrationMarkers.d.ts.map +1 -1
  20. package/dist/hydrationMarkers.js +25 -23
  21. package/dist/message/index.d.ts +2 -1
  22. package/dist/message/index.d.ts.map +1 -1
  23. package/dist/message/index.js +1 -1
  24. package/dist/message/public.d.ts +2 -1
  25. package/dist/message/public.d.ts.map +1 -1
  26. package/dist/message/public.js +1 -1
  27. package/dist/runtime/runtime.d.ts +18 -14
  28. package/dist/runtime/runtime.d.ts.map +1 -1
  29. package/dist/runtime/runtime.js +99 -37
  30. package/dist/schema/index.d.ts +53 -8
  31. package/dist/schema/index.d.ts.map +1 -1
  32. package/dist/schema/index.js +179 -21
  33. package/dist/snabbdom/class.d.ts.map +1 -1
  34. package/dist/snabbdom/class.js +61 -5
  35. package/dist/snabbdom/style.d.ts.map +1 -1
  36. package/dist/snabbdom/style.js +5 -1
  37. package/dist/subscription/subscription.d.ts +1 -1
  38. package/dist/subscription/subscription.js +1 -1
  39. package/dist/test/apps/attributes.d.ts +7 -4
  40. package/dist/test/apps/attributes.d.ts.map +1 -1
  41. package/dist/test/apps/attributes.js +6 -6
  42. package/dist/test/apps/bubbling.d.ts +8 -2
  43. package/dist/test/apps/bubbling.d.ts.map +1 -1
  44. package/dist/test/apps/bubbling.js +10 -9
  45. package/dist/test/apps/colorPicker.d.ts +7 -6
  46. package/dist/test/apps/colorPicker.d.ts.map +1 -1
  47. package/dist/test/apps/colorPicker.js +8 -7
  48. package/dist/test/apps/contextMenu.d.ts +14 -4
  49. package/dist/test/apps/contextMenu.d.ts.map +1 -1
  50. package/dist/test/apps/contextMenu.js +8 -9
  51. package/dist/test/apps/counter.d.ts +52 -24
  52. package/dist/test/apps/counter.d.ts.map +1 -1
  53. package/dist/test/apps/counter.js +23 -34
  54. package/dist/test/apps/crashOnRender.d.ts +44 -28
  55. package/dist/test/apps/crashOnRender.d.ts.map +1 -1
  56. package/dist/test/apps/crashOnRender.js +17 -20
  57. package/dist/test/apps/drafts.d.ts +19 -8
  58. package/dist/test/apps/drafts.d.ts.map +1 -1
  59. package/dist/test/apps/drafts.js +10 -11
  60. package/dist/test/apps/feedSocket.d.ts +13 -13
  61. package/dist/test/apps/feedSocket.d.ts.map +1 -1
  62. package/dist/test/apps/feedSocket.js +17 -28
  63. package/dist/test/apps/fileUpload.d.ts +7 -6
  64. package/dist/test/apps/fileUpload.d.ts.map +1 -1
  65. package/dist/test/apps/fileUpload.js +9 -8
  66. package/dist/test/apps/formChild.d.ts +40 -24
  67. package/dist/test/apps/formChild.d.ts.map +1 -1
  68. package/dist/test/apps/formChild.js +31 -32
  69. package/dist/test/apps/interactions.d.ts +16 -11
  70. package/dist/test/apps/interactions.d.ts.map +1 -1
  71. package/dist/test/apps/interactions.js +21 -24
  72. package/dist/test/apps/keypress.d.ts +11 -11
  73. package/dist/test/apps/keypress.d.ts.map +1 -1
  74. package/dist/test/apps/keypress.js +12 -12
  75. package/dist/test/apps/login.d.ts +43 -23
  76. package/dist/test/apps/login.d.ts.map +1 -1
  77. package/dist/test/apps/login.js +17 -25
  78. package/dist/test/apps/logoutButton.d.ts +12 -6
  79. package/dist/test/apps/logoutButton.d.ts.map +1 -1
  80. package/dist/test/apps/logoutButton.js +15 -11
  81. package/dist/test/apps/mountPanel.d.ts +19 -20
  82. package/dist/test/apps/mountPanel.d.ts.map +1 -1
  83. package/dist/test/apps/mountPanel.js +22 -24
  84. package/dist/test/apps/multiRole.d.ts +6 -3
  85. package/dist/test/apps/multiRole.d.ts.map +1 -1
  86. package/dist/test/apps/multiRole.js +8 -7
  87. package/dist/test/apps/pointer.d.ts +13 -6
  88. package/dist/test/apps/pointer.d.ts.map +1 -1
  89. package/dist/test/apps/pointer.js +11 -10
  90. package/dist/test/apps/resumeUpload.d.ts +11 -14
  91. package/dist/test/apps/resumeUpload.d.ts.map +1 -1
  92. package/dist/test/apps/resumeUpload.js +17 -27
  93. package/dist/test/apps/scorePanel.d.ts +6 -3
  94. package/dist/test/apps/scorePanel.d.ts.map +1 -1
  95. package/dist/test/apps/scorePanel.js +8 -7
  96. package/dist/test/apps/selectiveKeys.d.ts +5 -1
  97. package/dist/test/apps/selectiveKeys.d.ts.map +1 -1
  98. package/dist/test/apps/selectiveKeys.js +13 -12
  99. package/dist/test/apps/uploads.d.ts +52 -30
  100. package/dist/test/apps/uploads.d.ts.map +1 -1
  101. package/dist/test/apps/uploads.js +20 -30
  102. package/dist/update/update.d.ts +4 -7
  103. package/dist/update/update.d.ts.map +1 -1
  104. package/package.json +1 -1
package/README.md CHANGED
@@ -18,12 +18,14 @@
18
18
 
19
19
  ---
20
20
 
21
- Foldkit is a TypeScript frontend framework built on [Effect](https://effect.website/) and architected like [Elm](https://guide.elm-lang.org/architecture/). One Model, one update function, one way to do things. No hooks, no local state, no hidden mutations. It's all in on Effect with no escape hatch, though a program doesn't have to own the whole page: [`Runtime.embed`](https://foldkit.dev/core/embedding) runs a Foldkit widget inside any existing app, React included. The same program also [renders on the server](https://foldkit.dev/core/server-rendering), at build time or per request, and hydrates in place.
21
+ Foldkit is a TypeScript frontend framework built on [Effect](https://effect.website/). It gives your entire application one architecture: a [Schema](https://effect.website/docs/schema/introduction/)-defined Model as the single source of truth, fact-named Messages, an exhaustive update function, and explicit Commands for side effects. Routing, server rendering, UI components, Submodels, and browser lifecycles all use that same Model and Message flow.
22
22
 
23
- Your Model is a [Schema](https://effect.website/docs/schema/introduction/) and side effects are values you return, not callbacks you fire. If you know Effect, Foldkit feels natural. If you're new to it, Foldkit is a good way in. Coming from React? [Start here](https://foldkit.dev/react/coming-from-react), or read the [same pixel-art editor built in both frameworks](https://foldkit.dev/react/foldkit-vs-react-side-by-side).
23
+ Foldkit uses [The Elm Architecture](https://guide.elm-lang.org/architecture/) instead of component-owned state and hook lifecycles. That discipline is a real commitment. Foldkit works best when the team wants shared conventions across the application and is ready to build on Effect throughout. If your backend already uses Effect, Foldkit carries the same tools and patterns into the browser: Schema, services, Streams, and scoped resources.
24
+
25
+ A Foldkit program can own the whole page or run as a widget inside an existing application, React included, through [`Runtime.embed`](https://foldkit.dev/core/embedding). The same program can [render on the server](https://foldkit.dev/core/server-rendering) at build time or per request, then hydrate in place. Coming from React? [Start here](https://foldkit.dev/react/coming-from-react), or compare the [same pixel-art editor built in both frameworks](https://foldkit.dev/react/foldkit-vs-react-side-by-side).
24
26
 
25
27
  > [!NOTE]
26
- > Foldkit is pre-1.0. The core API is stable, but breaking changes may occur in minor releases. See the [changelog](./CHANGELOG.md) for details.
28
+ > Foldkit is in beta and under active development. The core API is stable, but breaking changes may occur in minor releases. See the [changelog](./CHANGELOG.md) for details.
27
29
 
28
30
  ## Get Started
29
31
 
@@ -35,14 +37,14 @@ npx create-foldkit-app@latest
35
37
 
36
38
  ## Counter
37
39
 
38
- A complete Foldkit program. State lives in a single Model, events become Messages, and a pure function handles every transition. `main.ts` defines the program and `entry.ts` boots the runtime, so `main.ts` stays importable from tests without booting a runtime as a side effect.
40
+ A complete Foldkit program. State lives in a single Model, events become Messages, and a pure function handles every transition. `main.ts` defines the program and `entry.ts` boots the Runtime, so `main.ts` stays importable from tests without booting a Runtime as a side effect.
39
41
 
40
42
  ```ts
41
43
  // src/main.ts
42
- import { Match as M, Schema as S } from 'effect'
44
+ import { Schema as S } from 'effect'
43
45
  import { Command, Runtime } from 'foldkit'
44
46
  import { Document, HtmlBuilder } from 'foldkit/html'
45
- import { m } from 'foldkit/message'
47
+ import { defineMessageUnion } from 'foldkit/message'
46
48
  import { evo } from 'foldkit/struct'
47
49
 
48
50
  // MODEL
@@ -52,32 +54,23 @@ export type Model = typeof Model.Type
52
54
 
53
55
  // MESSAGE
54
56
 
55
- const ClickedDecrement = m('ClickedDecrement')
56
- const ClickedIncrement = m('ClickedIncrement')
57
- const ClickedReset = m('ClickedReset')
58
-
59
- export const Message = S.Union([
60
- ClickedDecrement,
61
- ClickedIncrement,
62
- ClickedReset,
63
- ])
57
+ export const Message = defineMessageUnion({
58
+ ClickedDecrement: {},
59
+ ClickedIncrement: {},
60
+ ClickedReset: {},
61
+ })
64
62
  export type Message = typeof Message.Type
65
63
 
66
64
  // UPDATE
67
65
 
68
- export const update = (
69
- model: Model,
70
- message: Message,
71
- ): readonly [Model, ReadonlyArray<Command.Command<Message>>] =>
72
- M.value(message).pipe(
73
- M.withReturnType<
74
- readonly [Model, ReadonlyArray<Command.Command<Message>>]
75
- >(),
76
- M.tagsExhaustive({
66
+ export const update = (model: Model, message: Message) =>
67
+ Message.match<readonly [Model, ReadonlyArray<Command.Command<Message>>]>(
68
+ message,
69
+ {
77
70
  ClickedDecrement: () => [evo(model, { count: count => count - 1 }), []],
78
71
  ClickedIncrement: () => [evo(model, { count: count => count + 1 }), []],
79
72
  ClickedReset: () => [evo(model, { count: () => 0 }), []],
80
- }),
73
+ },
81
74
  )
82
75
 
83
76
  // INIT
@@ -95,9 +88,9 @@ export const view = (model: Model, h: HtmlBuilder<Message>): Document => ({
95
88
  [],
96
89
  [
97
90
  h.p([], [model.count.toString()]),
98
- h.button([h.OnClick(ClickedDecrement())], ['-']),
99
- h.button([h.OnClick(ClickedReset())], ['Reset']),
100
- h.button([h.OnClick(ClickedIncrement())], ['+']),
91
+ h.button([h.OnClick(Message.ClickedDecrement())], ['-']),
92
+ h.button([h.OnClick(Message.ClickedReset())], ['Reset']),
93
+ h.button([h.OnClick(Message.ClickedIncrement())], ['+']),
101
94
  ],
102
95
  ),
103
96
  })
@@ -124,9 +117,9 @@ Source: [examples/counter](https://github.com/foldkit/foldkit/blob/main/examples
124
117
 
125
118
  ## What Ships With Foldkit
126
119
 
127
- A complete system, not a collection of libraries you stitch together. Each of these is documented in depth at [foldkit.dev](https://foldkit.dev).
120
+ Routing, server rendering, UI components, composition, and browser lifecycles all use the same Model and Message flow. The pieces below ship as one system and are documented in depth at [foldkit.dev](https://foldkit.dev).
128
121
 
129
- - **Commands**: Side effects as named Effects that return Messages and are run by the runtime.
122
+ - **Commands**: Side effects as named Effects that return Messages and are run by the Runtime.
130
123
  - **Routing**: Type-safe bidirectional routing from parser combinators. URLs parse to Routes, Routes build URLs.
131
124
  - **Subscriptions**: External event streams declared as a function of the Model.
132
125
  - **Managed Resources**: Model-driven lifecycle for WebSockets, AudioContext, and other long-lived handles.
@@ -146,9 +139,9 @@ A complete system, not a collection of libraries you stitch together. Each of th
146
139
  - **Slow Warnings**: Development warnings when update, view, patch, or Subscription extraction exceeds its budget.
147
140
  - **HMR**: Vite plugin with state-preserving hot module replacement. Change your view, keep your state.
148
141
 
149
- ## Correctness You (And Your LLM) Can See
142
+ ## AI-Assisted Development
150
143
 
151
- Every state change flows through one update function, and every side effect is declared explicitly. You don't have to hold a mental model of what runs when, you can point at it. That's what makes Foldkit unusually AI-friendly: the property that makes the code easy for humans to reason about makes it easy for an LLM to generate and review.
144
+ Every feature has the same visible structure: a Schema-defined Model, fact-named Messages, exhaustive update, and explicit Commands. AI-generated changes follow code paths a person can inspect and test. Foldkit DevTools and its MCP server expose the same Model and Message history while the application runs.
152
145
 
153
146
  ## Examples
154
147
 
@@ -1 +1 @@
1
- {"version":3,"file":"controlledDomState.d.ts","sourceRoot":"","sources":["../src/controlledDomState.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAoBhD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,GACvC,SAAS,OAAO,EAChB,cAAc,MAAM,EACpB,OAAO,OAAO,KACb,IAqDF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,EAChB,cAAc,MAAM,KACnB,IA0CF,CAAA;AAsFD,eAAO,MAAM,0BAA0B,GACrC,SAAS,OAAO,EAChB,OAAO,KAAK,KACX,IAYF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,sCAAsC,GACjD,SAAS,OAAO,EAChB,cAAc,MAAM,KACnB,IA0BF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACvC,SAAS,OAAO,EAChB,YAAY,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,KACxD,aAAa,CAAC,MAAM,CAuBtB,CAAA"}
1
+ {"version":3,"file":"controlledDomState.d.ts","sourceRoot":"","sources":["../src/controlledDomState.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAoBhD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,GACvC,SAAS,OAAO,EAChB,cAAc,MAAM,EACpB,OAAO,OAAO,KACb,IAyDF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,EAChB,cAAc,MAAM,KACnB,IA0CF,CAAA;AAsFD,eAAO,MAAM,0BAA0B,GACrC,SAAS,OAAO,EAChB,OAAO,KAAK,KACX,IAYF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,sCAAsC,GACjD,SAAS,OAAO,EAChB,cAAc,MAAM,KACnB,IA0BF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACvC,SAAS,OAAO,EAChB,YAAY,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,KACxD,aAAa,CAAC,MAAM,CAuBtB,CAAA"}
@@ -58,13 +58,17 @@ export const synchronizeControlledDefault = (element, propertyName, value) => {
58
58
  return;
59
59
  }
60
60
  if (propertyName === 'value' && tagName === 'select') {
61
+ // NOTE: set the live selection before copying it into the defaults. Two
62
+ // options can share one value, so `select.value` may already equal the
63
+ // controlled value while a later child `selected` still owns the live
64
+ // selection. The value setter gives the first matching option ownership.
65
+ Reflect.set(element, 'value', value);
61
66
  for (const option of element.querySelectorAll('option')) {
62
67
  if (option.defaultSelected !== option.selected) {
63
68
  option.defaultSelected = option.selected;
64
69
  }
65
70
  setBooleanDefaultAttribute(option, 'selected', option.selected);
66
71
  }
67
- Reflect.set(element, 'value', value);
68
72
  }
69
73
  };
70
74
  /** Clears default state owned by a controlled property that left a vnode.
@@ -32,10 +32,10 @@ export declare const controlledValueContent: (properties: Readonly<Record<string
32
32
  * tree serializes to an empty comment, mirroring how the runtime patches
33
33
  * `null` as a comment node.
34
34
  *
35
- * A hydratable render also stamps a digest of each vnode's key and identity, so
36
- * hydration can tell one logical entity from another; the raw key and the
37
- * compiler's source identity never appear in the markup. A render that is not
38
- * hydratable emits neither.
35
+ * A hydratable render also stamps a fingerprint of each vnode's key and
36
+ * identity, so hydration can tell one logical entity from another; the raw key
37
+ * and the compiler's source identity never appear in the markup. A render that
38
+ * is not hydratable emits neither.
39
39
  *
40
40
  * @internal Not part of the `foldkit/experimental/server` surface; `renderToString` is the public entry to serialization.
41
41
  */
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/serialize.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAGpD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAC,CAAA;AAsNF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAM1C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,KAAG,MAMpD,CAAA;AA0MD,eAAO,MAAM,sBAAsB,GACjC,YAAY,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,KACxD,MAAM,GAAG,SAMX,CAAA;AAugBD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,KAAK,GAAG,IAAI,EAClB,UAAU,gBAAgB,KACzB,MAUF,CAAA"}
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/serialize.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAGpD;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,oBAAoB,CAAC,EAAE,OAAO,CAAA;CAC/B,CAAC,CAAA;AAsNF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAM1C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,KAAG,MAMpD,CAAA;AA0MD,eAAO,MAAM,sBAAsB,GACjC,YAAY,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,KACxD,MAAM,GAAG,SAMX,CAAA;AA2hBD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,KAAK,GAAG,IAAI,EAClB,UAAU,gBAAgB,KACzB,MAUF,CAAA"}
@@ -532,7 +532,17 @@ const selectValueForChildren = (tagName, node, inherited) => {
532
532
  if (tagName !== 'select') {
533
533
  return inherited;
534
534
  }
535
- const value = node.data?.props?.['value'];
535
+ const properties = node.data?.props;
536
+ if (isClientOnlyProperty(properties, 'value')) {
537
+ throw new Error('[foldkit] Cannot server-render a native <select> with a client-only ' +
538
+ 'value property. Foldkit applies that value before options exist in ' +
539
+ 'a fresh client render and after they exist during hydration, so the ' +
540
+ 'two renders can select different options. CustomElement property ' +
541
+ 'factories belong on their declared Custom Element. Use h.Value(...) ' +
542
+ 'to give a native select one controlled selection to serialize and ' +
543
+ 'hydrate.');
544
+ }
545
+ const value = properties?.['value'];
536
546
  if (typeof value === 'string') {
537
547
  return {
538
548
  value,
@@ -611,14 +621,20 @@ const serializeElement = (output, context, node, selector, depth, extraAttribute
611
621
  }
612
622
  }
613
623
  // The server HTML does not otherwise carry a vnode's key or identity, so
614
- // hydration would adopt keyed children positionally. Stamp a digest of each so
615
- // hydration can verify it is adopting the same logical entity without the raw
616
- // key or source identity appearing in public markup; it strips the markers as
617
- // it adopts.
624
+ // hydration would adopt keyed children positionally. Stamp a fingerprint of
625
+ // each so hydration can verify it is adopting the same logical entity without
626
+ // the raw key or source identity appearing in public markup; it strips the
627
+ // markers as it adopts.
618
628
  if (context.emitHydrationMarkers) {
619
629
  if (node.key !== undefined) {
620
630
  const keyMarker = hydrationKeyMarker(node.key);
621
631
  if (keyMarker === undefined) {
632
+ if (typeof node.key === 'number' && Number.isNaN(node.key)) {
633
+ throw new Error('[foldkit] Cannot server-render an element keyed by NaN as ' +
634
+ 'hydratable. NaN is not equal to itself, so hydration cannot ' +
635
+ 'tell whether the server and client mean the same element. ' +
636
+ 'Key hydratable elements by a string or a number other than NaN.');
637
+ }
622
638
  throw new Error('[foldkit] Cannot server-render an element keyed by a symbol. A ' +
623
639
  'symbol key cannot be compared across the server and the client ' +
624
640
  '(a local symbol is a new value in every realm, so hydration ' +
@@ -766,10 +782,10 @@ const serializeNode = (output, context, node, depth, extraAttributes, selectValu
766
782
  * tree serializes to an empty comment, mirroring how the runtime patches
767
783
  * `null` as a comment node.
768
784
  *
769
- * A hydratable render also stamps a digest of each vnode's key and identity, so
770
- * hydration can tell one logical entity from another; the raw key and the
771
- * compiler's source identity never appear in the markup. A render that is not
772
- * hydratable emits neither.
785
+ * A hydratable render also stamps a fingerprint of each vnode's key and
786
+ * identity, so hydration can tell one logical entity from another; the raw key
787
+ * and the compiler's source identity never appear in the markup. A render that
788
+ * is not hydratable emits neither.
773
789
  *
774
790
  * @internal Not part of the `foldkit/experimental/server` surface; `renderToString` is the public entry to serialization.
775
791
  */
@@ -6,9 +6,10 @@ export declare const __assertNoDeclarativeShadowRoot: (html: string) => void;
6
6
  export declare const __assertNoLiveBaseElement: (html: string) => void;
7
7
  export declare const __assertNoDocumentStructureEscape: (html: string) => void;
8
8
  export { FOLDKIT_APP_ATTRIBUTE, FOLDKIT_FLAGS_ATTRIBUTE };
9
- /** The server render of one request: the stamped root markup (plus the Flags
10
- * payload script when the application declares Flags) and the `Document`
11
- * head fields for the host to place into its HTML template.
9
+ /** The server render of one request: the body markup and the `Document` head
10
+ * fields for the host to place into its HTML template. Hydratable output
11
+ * contains a stamped root and, when the application declares Flags, its
12
+ * payload script. Static output carries no handoff markers.
12
13
  *
13
14
  * @experimental Ships from `foldkit/experimental/server`; expect breaking changes while the API settles.
14
15
  */
@@ -171,6 +172,8 @@ export type StaticRenderOptions = CommonRenderOptions & Readonly<{
171
172
  }>;
172
173
  /** Options for {@link renderToString}. `runtimeId` names the application in the
173
174
  * root stamp and Flags payload; it defaults to `'app'` and must be non-empty.
175
+ * A nondefault `runtimeId` changes the root, Flags, and HMR pairing. It does
176
+ * not permit a second hydratable application in one document.
174
177
  *
175
178
  * A hydratable render (the default) requires `buildId`. Pass
176
179
  * `isHydratable: false` for static markup that nothing will hydrate, which
@@ -204,10 +207,10 @@ export type RenderUrlFlagsOptions<Flags> = RenderUrlOptions & RenderFlagsOptions
204
207
  *
205
208
  * Resolves `init` for the request (with the given Flags and URL when the
206
209
  * config declares them), runs the pure `view` under a no-op dispatch frame,
207
- * and serializes the resulting `Document` body. The root element is stamped
208
- * with {@link FOLDKIT_APP_ATTRIBUTE} and, when the config declares `Flags`,
209
- * the Schema-encoded Flags ride along in a JSON script tag so a hydrating
210
- * client boots from the same Model.
210
+ * and serializes the resulting `Document` body. For hydratable output, the
211
+ * root element is stamped with {@link FOLDKIT_APP_ATTRIBUTE} and, when the
212
+ * config declares `Flags`, the Schema-encoded Flags ride along in a JSON script
213
+ * tag so a hydrating client boots from the same Model.
211
214
  *
212
215
  * Commands returned by `init` are not run: the rendered HTML is the
213
216
  * post-`init` state, and the client runs those Commands after hydration.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,MAAM,EAGN,MAAM,EAEP,MAAM,QAAQ,CAAA;AAgBf,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,WAAW,EAGjB,MAAM,qBAAqB,CAAA;AAM5B,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,0BAA0B,CAAA;AAQjC,OAAO,EAAE,GAAG,EAAc,MAAM,oBAAoB,CAAA;AAmgBpD,eAAO,MAAM,+BAA+B,GAAI,MAAM,MAAM,KAAG,IAgB9D,CAAA;AA6BD,eAAO,MAAM,yBAAyB,GAAI,MAAM,MAAM,KAAG,IAgBxD,CAAA;AA0DD,eAAO,MAAM,iCAAiC,GAAI,MAAM,MAAM,KAAG,IAahE,CAAA;AAiSD,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,CAAA;AAIzD;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAAA;;;;AAEF;;;GAGG;AACH,qBAAa,UAAW,SAAQ,gBAA+B;IAC7D,GAAG,EAAE,MAAM,CAAA;CACZ,CAAC;CAAG;;;;AAEL;;;;;;GAMG;AACH,qBAAa,gBAAiB,SAAQ,sBAAqC;IACzE,KAAK,EAAE,OAAO,CAAA;CACf,CAAC;CAAG;;;;AAEL;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,KAAK,EAAE,OAAO,CAAA;CACf,CAAC;CAAG;;;;AAEL;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,sBAAqC;IACzE,SAAS,EAAE,MAAM,CAAA;CAClB,CAAC;CAAG;;;;AAEL;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,0BAExC;IACA,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CACvC,CAAC;CAAG;;;;AAEL;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,SAAQ,oBAAmC,EAAE,CAAC;CAAG;AAE7E;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB,cAAc,GACd,UAAU,GACV,gBAAgB,GAChB,kBAAkB,GAClB,gBAAgB,GAChB,oBAAoB,CAAA;AAExB,KAAK,UAAU,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAA;AAEjE;;;;;GAKG;AACH,MAAM,MAAM,iCAAiC,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,IACjE,QAAQ,CAAC;IACP,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,KAAK,UAAU,CAAC,KAAK,CAAC,CAAA;IACnD,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAA;CAC1D,CAAC,CAAA;AAEJ;;;GAGG;AACH,MAAM,MAAM,wBAAwB,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IAC9D,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,UAAU,CAAC,KAAK,CAAC,CAAA;IACrC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAA;CAC1D,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,MAAM,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,IAAI,QAAQ,CAAC;IACvE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAC7C,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC,KAAK,CAAC,CAAA;IACzC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAA;CAC1D,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAAC,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACvD,IAAI,EAAE,MAAM,UAAU,CAAC,KAAK,CAAC,CAAA;IAC7B,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAA;CAC1D,CAAC,CAAA;AASF,KAAK,mBAAmB,GAAG,QAAQ,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,mBAAmB,GACvD,QAAQ,CAAC;IACP,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAA;CAChB,CAAC,CAAA;AAEJ;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GACnD,QAAQ,CAAC;IACP,YAAY,EAAE,KAAK,CAAA;IACnB,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAC,CAAA;AAEJ;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,uBAAuB,GAAG,mBAAmB,CAAA;AAEzE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAC1C,QAAQ,CAAC;IACP,GAAG,EAAE,MAAM,CAAA;CACZ,CAAC,CAAA;AAEJ;;;GAGG;AACH,MAAM,MAAM,kBAAkB,CAAC,KAAK,IAAI,aAAa,GACnD,QAAQ,CAAC;IACP,KAAK,EAAE,KAAK,CAAA;CACb,CAAC,CAAA;AAEJ;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,CAAC,KAAK,IAAI,gBAAgB,GACzD,kBAAkB,CAAC,KAAK,CAAC,CAAA;AAgI3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAClD,MAAM,EAAE,iCAAiC,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAChE,OAAO,EAAE,qBAAqB,CAAC,KAAK,CAAC,GACpC,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;AAClD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAC3C,MAAM,EAAE,wBAAwB,CAAC,KAAK,EAAE,OAAO,CAAC,EAChD,OAAO,EAAE,gBAAgB,GACxB,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;AAClD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAClD,MAAM,EAAE,0BAA0B,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EACzD,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,GACjC,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA;AAKlD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAC3C,MAAM,EAAE,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,EACzC,OAAO,EAAE,aAAa,GACrB,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,WAAW,CAAC,CAAA"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/server.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Array as Array_, Context, Data, Effect, Option, Predicate, Schema, pipe, } from 'effect';
1
+ import { Array as Array_, Context, Data, Effect, Option, Predicate, Schema, String, pipe, } from 'effect';
2
2
  import { html as Parse5Html, defaultTreeAdapter, parse, parseFragment, } from 'parse5';
3
3
  import { HYDRATION_BUILD_ATTRIBUTE } from '../../buildToken.js';
4
4
  import { MATHML_NAMESPACE, SVG_NAMESPACE, parsedAttributeName, } from '../../domReflection.js';
@@ -857,11 +857,12 @@ export function renderToString(config, options) {
857
857
  // builds of one deployment or be shared by two that render differently.
858
858
  // Refusing is the only honest answer, and it names what to supply.
859
859
  const configuredBuildId = options?.buildId;
860
- if (isHydratable &&
861
- (configuredBuildId === undefined || configuredBuildId === '')) {
860
+ const isConfiguredBuildId = Predicate.isString(configuredBuildId) &&
861
+ !String.isEmpty(configuredBuildId);
862
+ if (isHydratable && !isConfiguredBuildId) {
862
863
  return yield* Effect.fail(new MissingBuildId());
863
864
  }
864
- const buildId = configuredBuildId ?? '';
865
+ const buildId = isConfiguredBuildId ? configuredBuildId : '';
865
866
  const url = hasRouting ? yield* parseUrl(options?.url ?? '') : undefined;
866
867
  const flagsHandoff = isHydratable && FlagsCodec !== undefined
867
868
  ? yield* encodeFlagsHandoff(FlagsCodec, options?.flags, runtimeId)
@@ -36,7 +36,11 @@ export type InjectIntoTemplateOptions = Readonly<{
36
36
  * top-level JSON Flags script. Static output may contain one element, text, or
37
37
  * comment root, or no body output. The helper rejects additional top-level
38
38
  * content, ambiguous handoff markers, and source that the HTML parser drops,
39
- * splits, moves, or reconstructs before insertion.
39
+ * splits, moves, or reconstructs before insertion. It also refuses to place a
40
+ * hydratable application in a template that already contains one. Static body
41
+ * output may be inserted beside a hydratable application because no runtime
42
+ * adopts it. Each insertion still applies that render's `Document` head
43
+ * fields.
40
44
  *
41
45
  * This helper is pure with no module state, so a host process may import it
42
46
  * directly even when the render itself must stay inside the server entry's
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/template.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,aAAa,CAAA;AA6oBpB;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C;iDAC6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,EAChB,UAAU,mBAAmB,EAC7B,UAAU,yBAAyB,KAClC,MA0MF,CAAA"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/experimental/server/template.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,aAAa,CAAA;AA6oBpB;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C;iDAC6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,kBAAkB,GAC7B,UAAU,MAAM,EAChB,UAAU,mBAAmB,EAC7B,UAAU,yBAAyB,KAClC,MAuNF,CAAA"}
@@ -462,7 +462,11 @@ const isContainerPlaceholder = (element, containerId) => element.tagName === 'di
462
462
  * top-level JSON Flags script. Static output may contain one element, text, or
463
463
  * comment root, or no body output. The helper rejects additional top-level
464
464
  * content, ambiguous handoff markers, and source that the HTML parser drops,
465
- * splits, moves, or reconstructs before insertion.
465
+ * splits, moves, or reconstructs before insertion. It also refuses to place a
466
+ * hydratable application in a template that already contains one. Static body
467
+ * output may be inserted beside a hydratable application because no runtime
468
+ * adopts it. Each insertion still applies that render's `Document` head
469
+ * fields.
466
470
  *
467
471
  * This helper is pure with no module state, so a host process may import it
468
472
  * directly even when the render itself must stay inside the server entry's
@@ -485,7 +489,7 @@ export const injectIntoTemplate = (template, rendered, options) => {
485
489
  }
486
490
  if (containers.length > 1) {
487
491
  throw new Error(`[foldkit] injectIntoTemplate found more than one <div id="${containerId}"></div> placeholder in the template. ` +
488
- 'Keep exactly one placeholder for each application root.');
492
+ 'Keep exactly one placeholder with that id.');
489
493
  }
490
494
  // The runtime id names an application across the whole page: it pairs a root
491
495
  // with its Flags payload, and it keys the Model and scroll position HMR
@@ -506,8 +510,16 @@ export const injectIntoTemplate = (template, rendered, options) => {
506
510
  'that id. A runtime id names one application for the whole page: it ' +
507
511
  'pairs a root with its Flags payload and keys the Model and scroll ' +
508
512
  'position hot reloading preserves, so two roots sharing one would ' +
509
- "take each other's state. Give each application its own " +
510
- '`runtimeId` when rendering.');
513
+ "take each other's state. Remove the duplicate root. Foldkit " +
514
+ 'supports one page-owning hydratable application per document.');
515
+ }
516
+ const existingApplications = collectMatching(document, element => attributeValue(element, FOLDKIT_APP_ATTRIBUTE) !== undefined);
517
+ if (existingApplications.length > 0) {
518
+ throw new Error('[foldkit] injectIntoTemplate is placing a hydratable application, ' +
519
+ 'but the page already holds a server-rendered application. Foldkit ' +
520
+ 'supports one page-owning application per document because each ' +
521
+ 'application owns the document metadata and installs document-wide ' +
522
+ 'navigation listeners. Render one application per page.');
511
523
  }
512
524
  const existingFlags = collectMatching(document, element => attributeValue(element, FOLDKIT_FLAGS_ATTRIBUTE) === renderedRuntimeId);
513
525
  if (existingFlags.length > 0) {
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAmUhD,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,EAAE,OAAO,KAAK,KAAG,MA4BnE,CAAA;AA+rBD,eAAO,MAAM,cAAc,GACzB,eAAe,OAAO,EACtB,WAAW,KAAK,GAAG,IAAI,EACvB,MAAM,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,EAC7B,SAAS,MAAM,KACd,KA0DF,CAAA"}
package/dist/hydrate.js CHANGED
@@ -161,6 +161,44 @@ const isControlledCurrentState = (element, propertyName) => {
161
161
  (propertyName === 'muted' && (tagName === 'audio' || tagName === 'video')));
162
162
  };
163
163
  const STALE_REFLECTED_PROPERTY = Symbol('foldkit/stale-reflected-property');
164
+ const NO_SELECTED_OPTION = -1;
165
+ // NOTE: a controlled select owns the effective state of every descendant
166
+ // option. The serializer marks the first matching option, and the browser's
167
+ // value setter does the same during a fresh render. Compare hydration against
168
+ // that parent-owned state instead of each option's shadowed `selected` prop.
169
+ const registerControlledSelectState = (element, vnode, status) => {
170
+ const properties = vnode.data?.props;
171
+ const authoredValue = properties?.['value'];
172
+ if (!(element instanceof HTMLSelectElement) ||
173
+ typeof authoredValue !== 'string' ||
174
+ isClientOnlyProperty(properties, 'value')) {
175
+ return;
176
+ }
177
+ const options = Array.from(element.options);
178
+ const selectedOption = options.find(option => option.value === authoredValue);
179
+ for (const option of options) {
180
+ status.controlledOptionSelection.set(option, option === selectedOption);
181
+ }
182
+ status.controlledSelectState.set(element, {
183
+ selectedIndex: selectedOption?.index ?? NO_SELECTED_OPTION,
184
+ value: selectedOption?.value ?? '',
185
+ });
186
+ };
187
+ const detectControlledSelectMismatch = (element, status) => {
188
+ const selectState = status.controlledSelectState.get(element);
189
+ if (selectState !== undefined &&
190
+ (Reflect.get(element, 'value') !== selectState.value ||
191
+ Reflect.get(element, 'selectedIndex') !== selectState.selectedIndex)) {
192
+ detectMismatch(status);
193
+ }
194
+ const isSelected = status.controlledOptionSelection.get(element);
195
+ if (isSelected !== undefined) {
196
+ if (Reflect.get(element, 'selected') !== isSelected ||
197
+ Reflect.get(element, 'defaultSelected') !== isSelected) {
198
+ detectMismatch(status);
199
+ }
200
+ }
201
+ };
164
202
  const seedReflectedProperties = (element, vnode, propertyNames, isCustomElement, status) => {
165
203
  const properties = vnode.data?.props;
166
204
  if (properties === undefined || propertyNames.length === 0) {
@@ -173,6 +211,12 @@ const seedReflectedProperties = (element, vnode, propertyNames, isCustomElement,
173
211
  if (attributeName === undefined) {
174
212
  continue;
175
213
  }
214
+ const isOwnedByControlledSelect = (name === 'value' && status.controlledSelectState.has(element)) ||
215
+ (name === 'selected' && status.controlledOptionSelection.has(element));
216
+ if (isOwnedByControlledSelect) {
217
+ seeded[name] = authored;
218
+ continue;
219
+ }
176
220
  const isEquivalent = !isCustomElement && isControlledCurrentState(element, name)
177
221
  ? Object.is(Reflect.get(element, name), authored)
178
222
  : element.getAttribute(attributeName) ===
@@ -231,6 +275,7 @@ export const __elementSignature = (element, vnode) => {
231
275
  // value is written by one module and then removed as stale state by another.
232
276
  // The hydration stamp is never seeded, so it is never removed.
233
277
  const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
278
+ detectControlledSelectMismatch(element, status);
234
279
  const classOwnedByModule = vnode.data?.class !== undefined &&
235
280
  htmlAttributeValue(vnode.data?.attrs, 'class') === undefined;
236
281
  const styleOwnedByModule = vnode.data?.style !== undefined &&
@@ -339,11 +384,11 @@ const matchesNamespace = (element, vnode) => (element.namespaceURI ?? HTML_NAMES
339
384
  // The server DOM does not encode a vnode's key or identity, so a positional
340
385
  // match on tag and namespace alone could adopt a different logical entity: a
341
386
  // reordered or stale keyed list would take over the wrong DOM node, and the user
342
- // state sitting on it. The serializer stamps a digest of key and identity; here
343
- // the same digest is computed for the vnode and compared, so a mismatch rebuilds
344
- // instead of transferring state to the wrong row or branch. A key type the
345
- // digest does not support (a symbol, which a hydratable render refuses) never
346
- // matches, so it rebuilds rather than adopting on a guess.
387
+ // state sitting on it. The serializer stamps a fingerprint of the key and
388
+ // identity; here the same fingerprint is computed for the vnode and compared,
389
+ // so a mismatch rebuilds instead of transferring state to the wrong row or
390
+ // branch. An unsupported key (NaN or a symbol, which a hydratable render
391
+ // refuses) never matches, so it rebuilds rather than adopting on a guess.
347
392
  const matchesAdoptionKey = (element, vnode) => {
348
393
  const serverKey = element.getAttribute(HYDRATION_KEY_ATTRIBUTE);
349
394
  if (vnode.key === undefined) {
@@ -351,9 +396,9 @@ const matchesAdoptionKey = (element, vnode) => {
351
396
  }
352
397
  const clientKey = hydrationKeyMarker(vnode.key);
353
398
  if (clientKey === undefined) {
354
- // A key type the digest cannot represent (a symbol, which a hydratable
355
- // render refuses) cannot be compared, so it never matches: rebuilding beats
356
- // adopting on a guess.
399
+ // An unsupported key (NaN or a symbol, which a hydratable render refuses)
400
+ // cannot be compared, so it never matches: rebuilding beats adopting on a
401
+ // guess.
357
402
  return false;
358
403
  }
359
404
  return serverKey === clientKey;
@@ -510,6 +555,7 @@ const adoptElement = (element, vnode, status) => {
510
555
  return clonePreparedTree(replaceHydrationElement(element, vnode));
511
556
  }
512
557
  const clone = cloneOf(vnode, element);
558
+ registerControlledSelectState(element, vnode, status);
513
559
  // Strip the hydration markers the serializer stamped: they are internal to the
514
560
  // handoff, already verified by the parent's positional walk before this call,
515
561
  // and must not remain on the adopted element.
@@ -804,6 +850,8 @@ export const __hydrateVNode = (hydrationRoot, nextVNode, seen, buildId) => {
804
850
  const status = {
805
851
  isMismatchDetected: false,
806
852
  adoptedSignatures: new Map(),
853
+ controlledOptionSelection: new Map(),
854
+ controlledSelectState: new Map(),
807
855
  };
808
856
  // The build token is checked again here, though the runtime has already
809
857
  // refused a mismatch before reading the handoff. This is the last line of
@@ -1,6 +1,7 @@
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. */
1
+ /** Attribute stamped on a hydratable server-rendered application root. Its
2
+ * nonempty value is the runtime id used to pair the root with its Flags
3
+ * payload and scope preserved HMR state. `makeApplication` locates the root;
4
+ * `Runtime.hydrate` adopts it. */
4
5
  export declare const FOLDKIT_APP_ATTRIBUTE = "data-foldkit-app";
5
6
  /** Attribute on the JSON script tag carrying the Schema-encoded flags the
6
7
  * server rendered with. A hydrating runtime decodes this payload instead of
@@ -1 +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"}
1
+ {"version":3,"file":"hydrationMarker.d.ts","sourceRoot":"","sources":["../src/hydrationMarker.ts"],"names":[],"mappings":"AAAA;;;mCAGmC;AACnC,eAAO,MAAM,qBAAqB,qBAAqB,CAAA;AAEvD;;;kBAGkB;AAClB,eAAO,MAAM,uBAAuB,uBAAuB,CAAA"}
@@ -1,6 +1,7 @@
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. */
1
+ /** Attribute stamped on a hydratable server-rendered application root. Its
2
+ * nonempty value is the runtime id used to pair the root with its Flags
3
+ * payload and scope preserved HMR state. `makeApplication` locates the root;
4
+ * `Runtime.hydrate` adopts it. */
4
5
  export const FOLDKIT_APP_ATTRIBUTE = 'data-foldkit-app';
5
6
  /** Attribute on the JSON script tag carrying the Schema-encoded flags the
6
7
  * server rendered with. A hydrating runtime decodes this payload instead of
@@ -1,8 +1,8 @@
1
1
  export declare const HYDRATION_KEY_ATTRIBUTE = "data-foldkit-key";
2
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).
3
+ /** The marker value for a vnode key, or `undefined` for a key the hydration
4
+ * handoff does not support (`NaN` or a symbol, both of which the server
5
+ * refuses in hydratable output).
6
6
  *
7
7
  * @internal
8
8
  */
@@ -1 +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"}
1
+ {"version":3,"file":"hydrationMarkers.d.ts","sourceRoot":"","sources":["../src/hydrationMarkers.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,uBAAuB,qBAAqB,CAAA;AACzD,eAAO,MAAM,4BAA4B,0BAA0B,CAAA;AAmCnE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,WAAW,KAAG,MAAM,GAAG,SAW9D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU,MAAM,KAAG,MAC7B,CAAA"}