ripple 0.3.128 → 0.4.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 (192) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/package.json +10 -4
  3. package/src/runtime/element.js +23 -6
  4. package/src/runtime/index-client.js +81 -41
  5. package/src/runtime/index-server.js +1 -0
  6. package/src/runtime/internal/client/blocks.js +173 -91
  7. package/src/runtime/internal/client/component.js +4 -30
  8. package/src/runtime/internal/client/composite.js +12 -13
  9. package/src/runtime/internal/client/constants.js +9 -0
  10. package/src/runtime/internal/client/context.js +12 -19
  11. package/src/runtime/internal/client/css.js +7 -9
  12. package/src/runtime/internal/client/events.js +178 -90
  13. package/src/runtime/internal/client/expression.js +45 -25
  14. package/src/runtime/internal/client/for.js +409 -129
  15. package/src/runtime/internal/client/head.js +10 -5
  16. package/src/runtime/internal/client/html.js +1 -0
  17. package/src/runtime/internal/client/hydration.js +152 -10
  18. package/src/runtime/internal/client/if.js +239 -50
  19. package/src/runtime/internal/client/index.js +4 -12
  20. package/src/runtime/internal/client/operations.js +40 -61
  21. package/src/runtime/internal/client/portal.js +163 -85
  22. package/src/runtime/internal/client/render.js +39 -39
  23. package/src/runtime/internal/client/rpc.js +3 -3
  24. package/src/runtime/internal/client/runtime.js +592 -433
  25. package/src/runtime/internal/client/selector.js +5 -2
  26. package/src/runtime/internal/client/template.js +133 -158
  27. package/src/runtime/internal/client/transport.js +38 -0
  28. package/src/runtime/internal/client/try.js +465 -426
  29. package/src/runtime/internal/client/types.d.ts +56 -23
  30. package/src/runtime/internal/client/utils.js +1 -3
  31. package/src/runtime/internal/server/dynamic.js +20 -21
  32. package/src/runtime/internal/server/index.js +325 -275
  33. package/src/runtime/internal/server/rpc.js +3 -2
  34. package/src/runtime/internal/server/transport.js +16 -0
  35. package/src/runtime/transport.js +21 -0
  36. package/tests/client/__snapshots__/for.test.tsrx.snap +0 -5
  37. package/tests/client/array/array.copy-within.test.tsrx +12 -12
  38. package/tests/client/array/array.derived.test.tsrx +46 -46
  39. package/tests/client/array/array.iteration.test.tsrx +10 -10
  40. package/tests/client/array/array.mutations.test.tsrx +20 -20
  41. package/tests/client/array/array.static.test.tsrx +11 -11
  42. package/tests/client/array/array.to-methods.test.tsrx +6 -6
  43. package/tests/client/async-suspend.test.tsrx +74 -75
  44. package/tests/client/basic/basic.attributes.test.tsrx +86 -83
  45. package/tests/client/basic/basic.collections.test.tsrx +12 -12
  46. package/tests/client/basic/basic.components.test.tsrx +99 -25
  47. package/tests/client/basic/basic.errors.test.tsrx +18 -18
  48. package/tests/client/basic/basic.events.test.tsrx +42 -42
  49. package/tests/client/basic/basic.get-set.test.tsrx +8 -8
  50. package/tests/client/basic/basic.reactivity.test.tsrx +73 -38
  51. package/tests/client/basic/basic.rendering.test.tsrx +25 -25
  52. package/tests/client/basic/basic.utilities.test.tsrx +3 -3
  53. package/tests/client/boundaries.test.tsrx +8 -8
  54. package/tests/client/boxed-locals.test.tsrx +452 -0
  55. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +6 -6
  56. package/tests/client/compiler/compiler.assignments.test.tsrx +9 -73
  57. package/tests/client/compiler/compiler.attributes.test.tsrx +23 -23
  58. package/tests/client/compiler/compiler.basic.test.tsrx +15 -60
  59. package/tests/client/compiler/compiler.tracked-access.test.tsrx +18 -43
  60. package/tests/client/compiler/compiler.try-in-function.test.tsrx +2 -2
  61. package/tests/client/composite/__snapshots__/composite.render.test.tsrx.snap +0 -1
  62. package/tests/client/composite/composite.dynamic-components.test.tsrx +77 -6
  63. package/tests/client/composite/composite.props.test.tsrx +149 -12
  64. package/tests/client/composite/composite.reactivity.test.tsrx +51 -52
  65. package/tests/client/composite/composite.render.test.tsrx +26 -9
  66. package/tests/client/css/scoped-styles.test.tsrx +8 -8
  67. package/tests/client/date.test.tsrx +42 -42
  68. package/tests/client/derived-release.test.tsrx +370 -0
  69. package/tests/client/dynamic-elements.test.tsrx +45 -45
  70. package/tests/client/events.test.tsrx +97 -71
  71. package/tests/client/for-selector.test.tsrx +14 -14
  72. package/tests/client/for-single-root-items.test.tsrx +118 -3
  73. package/tests/client/for.test.tsrx +332 -31
  74. package/tests/client/head.test.tsrx +19 -19
  75. package/tests/client/html.test.tsrx +11 -9
  76. package/tests/client/if-block.test.tsrx +80 -0
  77. package/tests/client/map.test.tsrx +16 -16
  78. package/tests/client/media-query.test.tsrx +7 -7
  79. package/tests/client/portal.test.tsrx +224 -26
  80. package/tests/client/primitive-text.test.tsrx +50 -0
  81. package/tests/client/prop-kind-switch.test.tsrx +141 -0
  82. package/tests/client/ref.test.tsrx +17 -17
  83. package/tests/client/return.test.tsrx +26 -3
  84. package/tests/client/root-boundary.test.tsrx +42 -0
  85. package/tests/client/scoped-flush.test.tsrx +8 -8
  86. package/tests/client/set.test.tsrx +6 -6
  87. package/tests/client/svg.test.tsrx +11 -11
  88. package/tests/client/switch.test.tsrx +44 -44
  89. package/tests/client/track-async-hydration.test.tsrx +4 -4
  90. package/tests/client/try.test.tsrx +151 -83
  91. package/tests/client/tsx.test.tsrx +43 -40
  92. package/tests/client/url/url.derived.test.tsrx +6 -6
  93. package/tests/client/url-search-params/url-search-params.derived.test.tsrx +8 -8
  94. package/tests/client/url-search-params/url-search-params.iteration.test.tsrx +10 -10
  95. package/tests/client/url-search-params/url-search-params.mutation.test.tsrx +10 -10
  96. package/tests/client/url-search-params/url-search-params.retrieval.test.tsrx +18 -18
  97. package/tests/client/url-search-params/url-search-params.serialization.test.tsrx +2 -2
  98. package/tests/hydration/basic.test.js +74 -1
  99. package/tests/hydration/build-components.js +22 -1
  100. package/tests/hydration/compiled/client/basic.js +1142 -713
  101. package/tests/hydration/compiled/client/composite.js +151 -100
  102. package/tests/hydration/compiled/client/events.js +184 -171
  103. package/tests/hydration/compiled/client/for.js +1472 -1132
  104. package/tests/hydration/compiled/client/fragment-cursor.js +2341 -0
  105. package/tests/hydration/compiled/client/head.js +254 -201
  106. package/tests/hydration/compiled/client/hmr.js +54 -41
  107. package/tests/hydration/compiled/client/html-in-template.js +54 -39
  108. package/tests/hydration/compiled/client/html.js +1633 -1263
  109. package/tests/hydration/compiled/client/if-children.js +420 -270
  110. package/tests/hydration/compiled/client/if-fragment-controlflow.js +363 -317
  111. package/tests/hydration/compiled/client/if.js +229 -219
  112. package/tests/hydration/compiled/client/mixed-control-flow.js +420 -404
  113. package/tests/hydration/compiled/client/nested-control-flow.js +1326 -1277
  114. package/tests/hydration/compiled/client/portal.js +87 -95
  115. package/tests/hydration/compiled/client/reactivity.js +135 -101
  116. package/tests/hydration/compiled/client/return.js +28 -19
  117. package/tests/hydration/compiled/client/streaming.js +390 -306
  118. package/tests/hydration/compiled/client/switch.js +258 -266
  119. package/tests/hydration/compiled/client/track-async-serialization.js +448 -280
  120. package/tests/hydration/compiled/client/try.js +173 -129
  121. package/tests/hydration/compiled/client/typed-text.js +71 -0
  122. package/tests/hydration/compiled/fixtures/money.js +10 -0
  123. package/tests/hydration/compiled/server/basic.js +123 -6
  124. package/tests/hydration/compiled/server/composite.js +16 -30
  125. package/tests/hydration/compiled/server/events.js +66 -18
  126. package/tests/hydration/compiled/server/for.js +117 -41
  127. package/tests/hydration/compiled/server/fragment-cursor.js +1687 -0
  128. package/tests/hydration/compiled/server/head.js +20 -20
  129. package/tests/hydration/compiled/server/hmr.js +2 -2
  130. package/tests/hydration/compiled/server/html.js +70 -238
  131. package/tests/hydration/compiled/server/if-children.js +107 -15
  132. package/tests/hydration/compiled/server/if.js +11 -15
  133. package/tests/hydration/compiled/server/mixed-control-flow.js +8 -8
  134. package/tests/hydration/compiled/server/portal.js +14 -22
  135. package/tests/hydration/compiled/server/reactivity.js +14 -14
  136. package/tests/hydration/compiled/server/streaming.js +19 -19
  137. package/tests/hydration/compiled/server/switch.js +8 -8
  138. package/tests/hydration/compiled/server/track-async-serialization.js +150 -23
  139. package/tests/hydration/compiled/server/try.js +8 -8
  140. package/tests/hydration/compiled/server/typed-text.js +17 -0
  141. package/tests/hydration/components/basic.tsrx +44 -8
  142. package/tests/hydration/components/composite.tsrx +2 -2
  143. package/tests/hydration/components/events.tsrx +25 -25
  144. package/tests/hydration/components/for.tsrx +115 -67
  145. package/tests/hydration/components/fragment-cursor.tsrx +650 -0
  146. package/tests/hydration/components/head.tsrx +16 -16
  147. package/tests/hydration/components/hmr.tsrx +2 -2
  148. package/tests/hydration/components/html.tsrx +7 -7
  149. package/tests/hydration/components/if-children.tsrx +72 -25
  150. package/tests/hydration/components/if.tsrx +18 -18
  151. package/tests/hydration/components/mixed-control-flow.tsrx +11 -11
  152. package/tests/hydration/components/portal.tsrx +3 -3
  153. package/tests/hydration/components/reactivity.tsrx +16 -16
  154. package/tests/hydration/components/streaming.tsrx +20 -20
  155. package/tests/hydration/components/switch.tsrx +20 -20
  156. package/tests/hydration/components/text-types.ts +4 -0
  157. package/tests/hydration/components/track-async-serialization.tsrx +80 -26
  158. package/tests/hydration/components/try.tsrx +8 -8
  159. package/tests/hydration/components/typed-text.tsrx +22 -0
  160. package/tests/hydration/fixtures/money.js +10 -0
  161. package/tests/hydration/for.test.js +36 -0
  162. package/tests/hydration/fragment-cursor.test.js +111 -0
  163. package/tests/hydration/if-children.test.js +24 -0
  164. package/tests/hydration/streaming.test.js +19 -0
  165. package/tests/hydration/track-async-serialization.test.js +87 -2
  166. package/tests/hydration/tsconfig.text-types.json +10 -0
  167. package/tests/hydration/typed-text.test.js +34 -0
  168. package/tests/server/await.test.tsrx +3 -3
  169. package/tests/server/basic.attributes.test.tsrx +42 -42
  170. package/tests/server/basic.components.test.tsrx +64 -8
  171. package/tests/server/basic.test.tsrx +34 -32
  172. package/tests/server/compiler.test.tsrx +1 -70
  173. package/tests/server/composite.props.test.tsrx +6 -6
  174. package/tests/server/dynamic-elements.test.tsrx +31 -8
  175. package/tests/server/for.test.tsrx +94 -0
  176. package/tests/server/head.test.tsrx +11 -11
  177. package/tests/server/scoped-styles.test.tsrx +2 -2
  178. package/tests/server/streaming-ssr.test.tsrx +48 -43
  179. package/tests/server/track-async-serialization.test.tsrx +204 -21
  180. package/tests/server/try.test.tsrx +48 -48
  181. package/tests/types/transport.ts +40 -0
  182. package/tests/utils/vite-plugin-config.test.js +23 -0
  183. package/types/index.d.ts +48 -7
  184. package/types/server.d.ts +3 -0
  185. package/types/transport.d.ts +15 -0
  186. package/src/runtime/internal/client/switch.js +0 -113
  187. package/src/utils/errors.js +0 -13
  188. package/tests/client/lazy-array.test.tsrx +0 -31
  189. package/tests/client/lazy-destructuring.test.tsrx +0 -438
  190. package/tests/client/tracked-index-access.test.tsrx +0 -113
  191. package/tests/server/lazy-destructuring.test.tsrx +0 -595
  192. package/tests/server/tracked-index-access.test.tsrx +0 -76
package/CHANGELOG.md CHANGED
@@ -1,5 +1,272 @@
1
1
  # ripple
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1472](https://github.com/Ripple-TS/ripple/pull/1472)
8
+ [`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Lazy destructuring is removed
10
+ from the language (tsrx RFC
11
+ [#106](https://github.com/Ripple-TS/ripple/issues/106)). `&[ ... ]` and
12
+ `&{ ... }` are syntax errors now; a tracked value is read and written through
13
+ `.value`, and a `Tracked`, `Derived`, or `WritableDerived` object is passed to a
14
+ child as it is (`<Child {count} />`, or `count={track(() => count.value)}` for a
15
+ read-only view).
16
+
17
+ | Before | After |
18
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------------ |
19
+ | `let &[count] = track(0); count++; {count}` | `const count = track(0); count.value++; {count.value}` |
20
+ | `let &[count, countT] = track(0); <Child count={countT} />` | `const count = track(0); <Child {count} />` |
21
+ | `let &[double] = track(() => count * 2)` | `const double = track(() => count.value * 2)` |
22
+ | `function Card({ count: &[count] }: { count: Tracked<number> })` | `function Card({ count }: { count: Tracked<number> })` and `count.value` |
23
+
24
+ `Tracked<V>`, `Derived<V>`, and `WritableDerived<V>` are plain `{ value: V }`
25
+ shapes; the `[V, Tracked<V>]` tuple member, the runtime `[0]`/`[1]` accessors,
26
+ and the `lazy_array_*` helpers are gone, and `count[0]` reports the ordinary
27
+ "use `.value`" error. Rest and default patterns lower to native destructuring: a
28
+ keyed `@for` pattern with a rest element or a default is destructured once per
29
+ item change, and a destructuring assignment onto boxed `let`s writes the boxes
30
+ in place; the `fallback`, `exclude_from_object`, and `array_slice` runtime
31
+ helpers are gone. On the server a dynamic tag `<{tag} />` is lowered to
32
+ `_$_.dynamic_element(tag, props)`, with the tag and the element's own props
33
+ passed separately, so the props never carry an `is` entry.
34
+
35
+ ### Patch Changes
36
+
37
+ - [#1458](https://github.com/Ripple-TS/ripple/pull/1458)
38
+ [`bbc1445`](https://github.com/Ripple-TS/ripple/commit/bbc14455485e054c145ebc78f08a771d41ea16f1)
39
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Lighter list, `@if`, and
40
+ mount output on the client. A controlled `@for` (the sole content of its
41
+ element) appends its items into the parent instead of inserting before an empty
42
+ text anchor, and an `@if` at the root of such an item creates an anchor only
43
+ once it needs a position of its own (a branch swap, or a branch that renders
44
+ nothing), so a keyed list of `@if` items keeps no anchor nodes in the DOM;
45
+ `mount()` likewise renders into the target without an anchor node. An `@if`
46
+ block now renders its branch directly and owns the branch's DOM range, and a
47
+ list keeps its inputs in block state, so every `@if` and `@for` allocates one
48
+ block less and no closures. Text and class updates compare against the last
49
+ value in the render block's state instead of a cache on the DOM node; the
50
+ compiler emits that compared form for single updates as well.
51
+
52
+ A selector key whose subscribers all left stays in the selector's map for reuse
53
+ when the key comes back, and is swept once more than 1024 released keys pile up.
54
+
55
+ - [#1469](https://github.com/Ripple-TS/ripple/pull/1469)
56
+ [`b3a38ce`](https://github.com/Ripple-TS/ripple/commit/b3a38ce0aae66a9dcac0b663c30161eed0d7547a)
57
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Add custom serialization for
58
+ trackAsync hydration and RPC arguments/results using `transport` handlers in
59
+ ripple.config.ts. Register matching encoders and decoders automatically in
60
+ development and production, and expose `setTransport` for custom integrations.
61
+ Apps without a transport retain the plain JSON hydration fast path. Provide a
62
+ browser entry for config helpers so importing defineConfig does not load the
63
+ Vite plugin's Node.js dependencies during hydration. A configured hydration
64
+ payload embeds devalue's flattened form directly, so the client revives it
65
+ without a second string encoding.
66
+
67
+ - [#1460](https://github.com/Ripple-TS/ripple/pull/1460)
68
+ [`2ef5da5`](https://github.com/Ripple-TS/ripple/commit/2ef5da52b26fc13e11537dd28df0e2b376ca0f52)
69
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Cheaper element refs,
70
+ effects, and component templates on the client. A `ref` is one effect block
71
+ keyed on its state instead of a render block that re-evaluates the thunk and
72
+ creates a branch and an effect for it, since the ref value is read untracked and
73
+ fixed for the life of the enclosing block. Effects deferred until a component
74
+ has rendered are recorded as flat entries, and an effect's first run skips the
75
+ child and teardown sweep. A tracked value written during a flush keeps its
76
+ previous value for teardowns on itself instead of in a map, and `flushSync`
77
+ releases those values when it finishes (they were only released by the microtask
78
+ flush before).
79
+
80
+ The compiler types the bindings of a destructured pattern
81
+ (`{ item }: { item: Item }` as component props, or `const { item } = props` from
82
+ a typed initializer), so their property reads lower to direct text and attribute
83
+ writes. A component whose body has setup statements beside a single root element
84
+ clones that element instead of a fragment.
85
+
86
+ - [#1470](https://github.com/Ripple-TS/ripple/pull/1470)
87
+ [`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193)
88
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Component props are plain
89
+ objects, evaluated once. A call site such as `<Child a={x} b="y" />` compiles to
90
+ the object literal `{ a: x, b: 'y' }` and passes it to the component as is: no
91
+ getters, no props class, no Proxy for spreads, no `Props` helpers, and no
92
+ `Object.keys`/spread lowering, so props behave like any other object in `.tsrx`
93
+ and `.ts` code alike. A prop expression is evaluated when the component is
94
+ called, so a child that must follow a change now receives a live value
95
+ explicitly: a tracked object (`count={count}`) or a derived
96
+ (`title={track(() => label + count.value)}`), read through `.value`.
97
+ `count={count.value}` passes the current number. `track(fn)` is typed
98
+ `Derived<V>` with a read-only `value`; `track(fn, get, set)` or
99
+ `track(fn, undefined, true)` is a `WritableDerived<V>`, and a write to a
100
+ read-only derived warns in development. A dynamic element
101
+ (`<{tag} class={x} />`) still updates its attributes reactively. `Props.keys`,
102
+ `Props.values`, `Props.entries`, `Props.has`, `Props.spread`, `Props.rest`,
103
+ `Props.ownSymbolKeys` and `Props.ownAllKeys` are removed, as they are plain
104
+ `Object` operations now.
105
+
106
+ - [#1457](https://github.com/Ripple-TS/ripple/pull/1457)
107
+ [`020d269`](https://github.com/Ripple-TS/ripple/commit/020d2699f8e1333367ac9f7abe9247bab55e272d)
108
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fix hydration when a fragment
109
+ root or a slot is followed by siblings. The compiler's closing `next(n)` for a
110
+ fragment root now counts from wherever the cursor was last positioned, so a
111
+ trailing element navigated to for an event or attribute, tracked text, a
112
+ control-flow block, a component, or a `{expression}` no longer makes the cursor
113
+ overshoot or fall short of the fragment's last node; a trailing element that was
114
+ descended into is popped back to. Control-flow bodies nested inside an element
115
+ now emit that `next(n)` too. At runtime, a `{expression}` leaves the cursor on
116
+ its own end marker like every other block, and a multi-node branch or `@for`
117
+ item steps to its block's end marker after appending, so the next sibling, item,
118
+ or component adopts the right node.
119
+
120
+ - [#1462](https://github.com/Ripple-TS/ripple/pull/1462)
121
+ [`453bf4b`](https://github.com/Ripple-TS/ripple/commit/453bf4ba8b1b31963df26696545e446ab4b1658e)
122
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster keyed lists and
123
+ lighter component output on the client. A `@for` keyed by the item itself
124
+ (`key item`) keys by identity with no key callback, so a re-run compares the
125
+ item arrays directly and skips the per-item value update; a same-length keyed
126
+ re-run rewrites the list's block array in place instead of allocating a new one,
127
+ with the unchanged prefix and suffix skipped by plain compares. Static child
128
+ components that follow their template siblings append into the parent element
129
+ instead of inserting before a `<!>` placeholder, so a component with trailing
130
+ child components clones fewer nodes and leaves no comment anchors in the DOM.
131
+ The compiler also infers a primitive text type through a call whose callee is
132
+ declared to return one (a binding typed as a function type, an `as` assertion, a
133
+ function with a declared return type, or a method or function-typed property of
134
+ an annotated object), so such text lowers to `set_text` instead of a generic
135
+ expression block.
136
+
137
+ - [#1463](https://github.com/Ripple-TS/ripple/pull/1463)
138
+ [`411809f`](https://github.com/Ripple-TS/ripple/commit/411809fb38f25b259a1078a901839318e4207871)
139
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster cold hydration and
140
+ server rendering. `hydrate()` accepts `rootBoundary: false` like `mount()`,
141
+ stepping over the server's root marker itself instead of rendering under a `try`
142
+ block (a streamed shell whose root suspended still hydrates under the default
143
+ boundary, which alone can adopt and activate its slot). The client runtime
144
+ compiles less on a cold page: the hydration cursor steps live in one module and
145
+ assign the cursor directly, a list's first render never compiles the diff, a
146
+ template's clone path stays out of its hydrating closure, `run_block` keeps
147
+ re-run and teardown handling in separate functions, and the compiler adopts an
148
+ element's lone text child in place (`hydrate_text`) with no `pop()`. A `@for`
149
+ whose `index` nothing reads allocates no tracked index per item. Removing the
150
+ server's inline styles now starts from an animation frame rather than inside
151
+ `mount()`/`hydrate()`.
152
+
153
+ On the server, `render()` joins its buffer tree with a plain walk instead of
154
+ `flat(Infinity).join('')`, `escape()` returns strings without `&`, `<` or `"`
155
+ after a single regex test, and an `Output` allocates its stream, css and
156
+ async-operation collections only for the root or on first use.
157
+
158
+ - [#1453](https://github.com/Ripple-TS/ripple/pull/1453)
159
+ [`82bc9ee`](https://github.com/Ripple-TS/ripple/commit/82bc9ee585c6b273bf69890bc2f230aba922bf19)
160
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Speed up portals and root
161
+ event delegation. `<Portal>` now lowers to a dedicated `portal()` runtime call,
162
+ portal content appends straight into its target without a placeholder node,
163
+ delegated root listeners are shared per target instead of per portal, event
164
+ registration no longer scans every root target, and `@if` blocks and compiled
165
+ `event()` calls allocate fewer closures. `mount()` accepts `rootBoundary: false`
166
+ to render without the default root try/pending boundary. Cold code paths
167
+ (keyed-list diff, block error handling, effect-phase flushing, hydration
168
+ branches) live in their own functions so a first mount compiles less. A
169
+ component whose root is `@if`, `@for`, `@switch`, or `@try` now re-renders at
170
+ its own position when it is portal content or one of an element's all-component
171
+ children, instead of at the end of the parent; `mount()`/`hydrate()` disposers
172
+ are safe to call more than once.
173
+
174
+ - [#1470](https://github.com/Ripple-TS/ripple/pull/1470)
175
+ [`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193)
176
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Cheaper component
177
+ instantiation and `@if` chains on the client. An `@else if`, and a branch whose
178
+ only statement is another `@if`, fold into the enclosing `@if` block on both the
179
+ client and the server, so a chain costs one block and one hydration boundary. An
180
+ `@if` whose condition reads no tracked state renders its branch directly under
181
+ the enclosing block with no block of its own (a block without dependencies is
182
+ never scheduled, so nothing is lost); a dynamic condition keeps its block and
183
+ adopts the dependencies recorded while it was evaluated. A module-level
184
+ component also carries a direct render entry so `render_component` skips the
185
+ element it would otherwise return. An `@if` condition and its branches compile
186
+ to module-level functions when they capture at most one local (passed through
187
+ the if runtime), a condition returns the branch function to render instead of
188
+ calling a selection callback, and a render block's function is module-level with
189
+ its captured locals carried on the block state, so a component instantiation
190
+ creates no closures for its ifs or its reactive text and attribute updates.
191
+ `Context.get()` walks a chain of the context entries that were set rather than
192
+ every ancestor component. A module-level component's render body is a
193
+ module-level function that receives the props from the element instead of a
194
+ closure created per instantiation, `Context.get()`/`set()` calls on a
195
+ `new Context()` binding skip the `with_scope` wrapper, and `mount()` only
196
+ schedules the SSR style removal when the page has server-rendered inline styles.
197
+ A `@switch` now lowers onto the same runtime as `@if`: its selector returns the
198
+ case to render, so a `@switch` over a value that reads no tracked state renders
199
+ its case with no block, its cases are module-level functions when they capture
200
+ at most one local, and the separate switch runtime is gone. The probe that
201
+ decides whether an `@if` needs a block now renders a static branch itself, one
202
+ runtime call instead of four. A module-level component with a destructuring
203
+ parameter gets the direct render entry as well, and an `@if` or `@switch` is
204
+ hoisted whatever number of locals it captures: several travel as one object
205
+ literal destructured in the hoisted signatures. A binding that hoisted template
206
+ code writes (a `ref={name}` setter, a handler that assigns it) or that is
207
+ reassigned after template code reads it is compiled to a `{ v }` box, so the
208
+ hoisted functions share the variable with the component exactly as a closure
209
+ would: a plain `let`, a name inside a destructuring pattern of any shape, a
210
+ function or catch parameter (reboxed first thing in the body), and a `let`
211
+ written through a destructuring or `for...of` assignment target.
212
+
213
+ - [#1473](https://github.com/Ripple-TS/ripple/pull/1473)
214
+ [`eb638ea`](https://github.com/Ripple-TS/ripple/commit/eb638ea4a3c73b7ecb98ccedfffc576661dc67b3)
215
+ Thanks [@leonidaz](https://github.com/leonidaz)! - A derived created during a
216
+ block's run (a `track(() => ...)` or `tracked.readOnly()` in an `@if` branch, a
217
+ keyed `@for` item, a render expression, or component setup) is now unsubscribed
218
+ from its sources when that block reruns or is destroyed, unless a reader that is
219
+ still alive holds it. Before, such deriveds stayed subscribed until the block
220
+ that owned them was destroyed, so a branch that toggled repeatedly accumulated
221
+ one stale derived per toggle, each walked on every write to the source. The
222
+ bookkeeping lives in a side table keyed by the creating block, so blocks that
223
+ create no deriveds pay nothing.
224
+
225
+ - [#1466](https://github.com/Ripple-TS/ripple/pull/1466)
226
+ [`2631804`](https://github.com/Ripple-TS/ripple/commit/2631804cd28980b213643ac4ef5e5bcb814eae57)
227
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Faster streaming SSR. A
228
+ resolved `trackAsync` value made of plain data (strings, finite numbers,
229
+ booleans, `null`, arrays and plain objects, each reachable once) now travels in
230
+ its hydration script as raw JSON that `JSON.parse` of the envelope yields
231
+ directly, instead of a devalue-encoded string; values JSON cannot represent
232
+ (`undefined`, `NaN`, `-0`, bigints, Dates, Maps, Sets, shared references,
233
+ cycles) keep the devalue encoding, and the client reads either form. Boundaries
234
+ that settle in the same task (data arriving together, a batch of promises
235
+ resolved by one timer or I/O callback) stream as one chunk, flushed once the
236
+ task's microtasks have drained; closing the stream flushes whatever is still
237
+ queued first. `devalue` is updated to 5.9.2, with the stringify performance
238
+ patch proposed upstream in sveltejs/devalue#190 applied through pnpm's
239
+ `patchedDependencies` until it is released.
240
+
241
+ - [#1472](https://github.com/Ripple-TS/ripple/pull/1472)
242
+ [`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)
243
+ Thanks [@leonidaz](https://github.com/leonidaz)! - `tracked.readOnly()` returns
244
+ a read-only view of a `Tracked` or `WritableDerived`: a `Derived<V>` that
245
+ follows the value and rejects writes, for a child, function, or context that
246
+ should only read it. It is equivalent to `track(() => tracked.value)`. On a
247
+ derived that is already read-only, `readOnly()` returns the derived itself.
248
+
249
+ - [#1465](https://github.com/Ripple-TS/ripple/pull/1465)
250
+ [`8ace206`](https://github.com/Ripple-TS/ripple/commit/8ace206bfed2cd80b758e6e72f94084d2ffe1455)
251
+ Thanks [@leonidaz](https://github.com/leonidaz)! - `@try` blocks and the root
252
+ boundary keep their state in one explicit `TryState` object, and the pending,
253
+ catch, request and streaming helpers are module functions that take it. A
254
+ boundary allocates its state and one branch closure instead of a closure per
255
+ helper.
256
+ - Updated dependencies
257
+ [[`bbc1445`](https://github.com/Ripple-TS/ripple/commit/bbc14455485e054c145ebc78f08a771d41ea16f1),
258
+ [`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
259
+ [`2ef5da5`](https://github.com/Ripple-TS/ripple/commit/2ef5da52b26fc13e11537dd28df0e2b376ca0f52),
260
+ [`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
261
+ [`020d269`](https://github.com/Ripple-TS/ripple/commit/020d2699f8e1333367ac9f7abe9247bab55e272d),
262
+ [`453bf4b`](https://github.com/Ripple-TS/ripple/commit/453bf4ba8b1b31963df26696545e446ab4b1658e),
263
+ [`411809f`](https://github.com/Ripple-TS/ripple/commit/411809fb38f25b259a1078a901839318e4207871),
264
+ [`82bc9ee`](https://github.com/Ripple-TS/ripple/commit/82bc9ee585c6b273bf69890bc2f230aba922bf19),
265
+ [`69d50d9`](https://github.com/Ripple-TS/ripple/commit/69d50d9849d8484a88d39b2908bb682d27ec7bc9),
266
+ [`0321791`](https://github.com/Ripple-TS/ripple/commit/03217912651e0a49edb8060a92602fd0e0abe193),
267
+ [`5d3e60d`](https://github.com/Ripple-TS/ripple/commit/5d3e60d86129ac20d23c86a923914ee44efea9d1)]:
268
+ - @tsrx/ripple@0.2.0
269
+
3
270
  ## 0.3.128
4
271
 
5
272
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.128",
6
+ "version": "0.4.0",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -29,6 +29,12 @@
29
29
  "default": "./src/runtime/index-server.js"
30
30
  },
31
31
  "./package.json": "./package.json",
32
+ "./transport": {
33
+ "types": "./types/transport.d.ts",
34
+ "worker": "./src/runtime/internal/server/transport.js",
35
+ "browser": "./src/runtime/internal/client/transport.js",
36
+ "default": "./src/runtime/internal/server/transport.js"
37
+ },
32
38
  "./server": {
33
39
  "types": "./types/server.d.ts",
34
40
  "default": "./src/server/index.js"
@@ -71,10 +77,10 @@
71
77
  },
72
78
  "dependencies": {
73
79
  "clsx": "^2.1.1",
74
- "devalue": "^5.8.1",
80
+ "devalue": "^5.9.2",
75
81
  "esm-env": "^1.2.2",
76
- "@tsrx/core": "^0.1.67",
77
- "@tsrx/ripple": "0.1.65"
82
+ "@tsrx/core": "^0.2.0",
83
+ "@tsrx/ripple": "0.2.0"
78
84
  },
79
85
  "devDependencies": {
80
86
  "@types/estree": "^1.0.8",
@@ -1,21 +1,38 @@
1
- const TSRX_ELEMENT = Symbol.for('ripple.element');
1
+ export const TSRX_ELEMENT = Symbol.for('ripple.element');
2
2
 
3
3
  /**
4
4
  * @typedef {{
5
5
  * render: Function;
6
+ * p: any;
6
7
  * [TSRX_ELEMENT]: true;
7
8
  * }} TSRXElement
8
9
  */
9
10
 
10
11
  /**
12
+ * Elements share one constructor so every instance has the same shape and the
13
+ * marker lives on the prototype instead of being defined per allocation.
11
14
  * @param {Function} render
15
+ * @param {any} p
16
+ * @this {TSRXElement}
17
+ */
18
+ function TSRXElementImpl(render, p) {
19
+ this.render = render;
20
+ this.p = p;
21
+ }
22
+ /** @type {any} */ (TSRXElementImpl.prototype)[TSRX_ELEMENT] = true;
23
+
24
+ /**
25
+ * @param {Function} render `(anchor, block, p) => …`
26
+ * @param {any} [p] a value handed back to `render` as its third argument: a
27
+ * module-level component render function receives the component's props
28
+ * this way instead of closing over them, so instantiating the component
29
+ * allocates no closure
12
30
  * @returns {TSRXElement}
13
31
  */
14
- export function tsrx_element(render) {
15
- return {
16
- render,
17
- [TSRX_ELEMENT]: true,
18
- };
32
+ export function tsrx_element(render, p = null) {
33
+ return /** @type {TSRXElement} */ (
34
+ /** @type {unknown} */ (new /** @type {any} */ (TSRXElementImpl)(render, p))
35
+ );
19
36
  }
20
37
 
21
38
  /**
@@ -1,28 +1,24 @@
1
- /** @import { RootBoundaryOptions } from '#client' */
1
+ /** @import { AppendIntoAnchor, RootBoundaryOptions } from '#client' */
2
2
 
3
3
  import { destroy_block, root } from './internal/client/blocks.js';
4
- import { handle_root_events } from './internal/client/events.js';
5
- import {
6
- get_first_child,
7
- get_next_sibling,
8
- init_operations,
9
- } from './internal/client/operations.js';
4
+ import { handle_root_events, release_root_events } from './internal/client/events.js';
5
+ import { init_operations } from './internal/client/operations.js';
10
6
  import { render_component } from './internal/client/component.js';
11
- import { create_anchor } from './internal/client/utils.js';
12
7
  import { try_block } from './internal/client/try.js';
13
- import { remove_ssr_css } from './internal/client/css.js';
8
+ import { remove_styles } from './internal/client/css.js';
14
9
  import { normalize_children } from './element.js';
15
10
  import {
16
- clear_track_hash_reference,
11
+ hydrate_next,
17
12
  hydrate_node,
18
13
  hydrating,
19
- set_hydrate_node,
20
- set_hydrating,
14
+ set_hydration,
15
+ track_hash_reference,
21
16
  } from './internal/client/hydration.js';
22
17
  import { COMMENT_NODE, HYDRATION_START } from '../constants.js';
23
18
 
24
19
  // Re-export JSX runtime functions for jsxImportSource: "ripple"
25
20
  export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
21
+ export { set_transport as setTransport } from './internal/client/transport.js';
26
22
  export {
27
23
  UNINITIALIZED,
28
24
  TRACKED_UPDATED,
@@ -31,7 +27,7 @@ export {
31
27
  } from './internal/client/constants.js';
32
28
 
33
29
  /**
34
- * @param {Node} anchor
30
+ * @param {Node | AppendIntoAnchor} anchor
35
31
  * @param {(anchor: Node) => void} render_content
36
32
  * @param {RootBoundaryOptions | undefined} boundary
37
33
  * @returns {void}
@@ -55,6 +51,7 @@ function render_root_boundary(anchor, render_content, boundary) {
55
51
  render_component(Pending, pending_anchor, {});
56
52
  }
57
53
  },
54
+ true,
58
55
  );
59
56
  }
60
57
 
@@ -63,71 +60,104 @@ function render_root_boundary(anchor, render_content, boundary) {
63
60
  * get, so `mount`/`hydrate` accept a component function as the `children`
64
61
  * prop.
65
62
  *
66
- * @param {Record<string, any> | undefined} props
63
+ * @param {Record<string, any>} props
67
64
  * @returns {Record<string, any>}
68
65
  */
69
66
  function normalize_props(props) {
70
- if (props?.children != null) {
71
- return { ...props, children: normalize_children(props.children) };
72
- }
73
-
74
- return props || {};
67
+ return { ...props, children: normalize_children(props.children) };
75
68
  }
76
69
 
77
70
  /**
71
+ * `rootBoundary` configures the default `try`/`pending`/`catch` boundary the
72
+ * app is rendered under; `false` renders without one. Without a root
73
+ * boundary, `trackAsync()` must sit inside a user `@try` block, and errors
74
+ * that escape one propagate out of the flush.
78
75
  * @param {Function} component
79
- * @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions }} options
76
+ * @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions | false }} options
80
77
  * @returns {() => void}
81
78
  */
82
79
  export function mount(component, options) {
83
80
  init_operations();
84
- remove_ssr_css();
81
+ // Only a page with server-rendered inline styles has anything to remove.
82
+ // Requesting a frame otherwise makes the browser attach layout to the
83
+ // mounted tree before the app's next update, which slows a teardown that
84
+ // would have preceded the first paint.
85
+ if (document.querySelector('style[data-ripple-ssr]') !== null) {
86
+ requestAnimationFrame(remove_styles);
87
+ }
85
88
 
86
- const props = normalize_props(options.props);
89
+ let props = options.props ?? {};
90
+ if (props.children != null) {
91
+ props = normalize_props(props);
92
+ }
87
93
  const target = options.target;
88
- const anchor = create_anchor();
89
94
 
90
95
  // Clear target content in case of SSR
91
96
  if (target.firstChild) {
92
97
  target.textContent = '';
93
98
  }
94
99
 
95
- target.append(anchor);
100
+ // The app appends into the emptied target; a root that keeps inserting
101
+ // relative to its anchor materializes one (see `resolve_anchor`).
102
+ /** @type {AppendIntoAnchor} */
103
+ const anchor = { parent: target, into: true };
96
104
 
97
- const cleanup_events = handle_root_events(target);
105
+ /** @type {import('./internal/client/events.js').RootTargetRef | null} */
106
+ let events_ref = handle_root_events(target);
107
+
108
+ const root_boundary = options.rootBoundary;
98
109
 
99
110
  const _root = root(() => {
111
+ if (root_boundary === false) {
112
+ render_component(component, anchor, props);
113
+ return;
114
+ }
100
115
  render_root_boundary(
101
116
  anchor,
102
117
  (component_anchor) => {
103
118
  render_component(component, component_anchor, props);
104
119
  },
105
- options.rootBoundary,
120
+ root_boundary,
106
121
  );
107
122
  });
108
123
 
109
124
  return () => {
110
- cleanup_events();
125
+ // The disposer may be called again (a remount, HMR, a stale reference);
126
+ // only the first call owns the target's delegated-listener ref.
127
+ if (events_ref === null) return;
128
+ release_root_events(events_ref);
129
+ events_ref = null;
111
130
  destroy_block(_root);
112
131
  };
113
132
  }
114
133
 
115
134
  /**
135
+ * `rootBoundary` works as in {@link mount}: the app hydrates under a default
136
+ * `try` boundary, or with `false` directly under the root block. Server output
137
+ * always carries the root boundary markers; without a boundary, `hydrate()`
138
+ * steps over a plain `<!--[-->` itself. A streamed shell whose root suspended
139
+ * starts with a `<!--[?N-->` / `<!--[!N-->` slot instead, and only a boundary
140
+ * can adopt its fallback and activate the chunk, so that shell hydrates under
141
+ * the default boundary regardless.
116
142
  * @param {Function} component
117
- * @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions }} options
143
+ * @param {{ props?: Record<string, any>, target: HTMLElement, rootBoundary?: RootBoundaryOptions | false }} options
118
144
  * @returns {() => void}
119
145
  */
120
146
  export function hydrate(component, options) {
121
147
  init_operations();
122
- remove_ssr_css();
148
+ requestAnimationFrame(remove_styles);
123
149
 
124
- const props = normalize_props(options.props);
150
+ let props = options.props ?? {};
151
+ if (props.children != null) {
152
+ props = normalize_props(props);
153
+ }
125
154
  const target = options.target;
126
155
  const was_hydrating = hydrating;
127
156
  const previous_hydrate_node = hydrate_node;
128
- let anchor = get_first_child(target);
157
+ let anchor = target.firstChild;
129
158
 
130
- const cleanup_events = handle_root_events(target);
159
+ /** @type {import('./internal/client/events.js').RootTargetRef | null} */
160
+ let events_ref = handle_root_events(target);
131
161
  let _root;
132
162
 
133
163
  try {
@@ -139,33 +169,43 @@ export function hydrate(component, options) {
139
169
  // marker instead of a plain `<!--[-->`
140
170
  !(/** @type {Comment} */ (anchor).data.startsWith(HYDRATION_START)))
141
171
  ) {
142
- anchor = get_next_sibling(anchor);
172
+ anchor = anchor.nextSibling;
143
173
  }
144
174
 
145
- set_hydrating(true);
146
- set_hydrate_node(/** @type {Comment} */ (anchor));
175
+ set_hydration(true, /** @type {Comment} */ (anchor));
176
+
177
+ const root_boundary = options.rootBoundary;
178
+ const marker = /** @type {Comment} */ (anchor);
147
179
 
148
180
  _root = root(() => {
181
+ if (root_boundary === false && marker.data === HYDRATION_START) {
182
+ // The root boundary's own hydration walk: consume the `<!--[-->`
183
+ // marker and render against it, as `try_block` does for the root.
184
+ hydrate_next();
185
+ render_component(component, marker, props);
186
+ return;
187
+ }
149
188
  render_root_boundary(
150
- /** @type {Comment} */ (anchor),
189
+ marker,
151
190
  (component_anchor) => {
152
191
  render_component(component, component_anchor, props);
153
192
  },
154
- options.rootBoundary,
193
+ root_boundary === false ? undefined : root_boundary,
155
194
  );
156
195
  });
157
196
  } catch (e) {
158
197
  throw e;
159
198
  } finally {
160
- set_hydrating(was_hydrating);
161
- set_hydrate_node(previous_hydrate_node, true);
199
+ set_hydration(was_hydrating, previous_hydrate_node);
162
200
  if (!was_hydrating) {
163
- clear_track_hash_reference();
201
+ track_hash_reference.clear();
164
202
  }
165
203
  }
166
204
 
167
205
  return () => {
168
- cleanup_events();
206
+ if (events_ref === null) return;
207
+ release_root_events(events_ref);
208
+ events_ref = null;
169
209
  destroy_block(_root);
170
210
  };
171
211
  }
@@ -1,6 +1,7 @@
1
1
  import { output_push, noop, tsrx_element } from './internal/server/index.js';
2
2
 
3
3
  export { Fragment } from '../jsx-runtime.js';
4
+ export { set_transport as setTransport } from './internal/server/transport.js';
4
5
  export { Context } from './internal/server/context.js';
5
6
  export {
6
7
  untrack,