ripple 0.3.9 → 0.3.10

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 (60) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +25 -15
  4. package/src/compiler/phases/2-analyze/index.js +35 -88
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +188 -56
  7. package/src/compiler/phases/3-transform/server/index.js +62 -40
  8. package/src/compiler/types/index.d.ts +9 -1
  9. package/src/compiler/types/parse.d.ts +2 -0
  10. package/src/compiler/utils.js +101 -1
  11. package/src/runtime/element.js +39 -0
  12. package/src/runtime/internal/client/composite.js +10 -6
  13. package/src/runtime/internal/client/expression.js +218 -0
  14. package/src/runtime/internal/client/index.js +4 -0
  15. package/src/runtime/internal/client/portal.js +12 -6
  16. package/src/runtime/internal/server/index.js +26 -1
  17. package/tests/client/basic/basic.components.test.ripple +85 -87
  18. package/tests/client/basic/basic.errors.test.ripple +4 -8
  19. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  20. package/tests/client/capture-error.js +12 -0
  21. package/tests/client/compiler/compiler.basic.test.ripple +76 -6
  22. package/tests/client/composite/composite.props.test.ripple +1 -3
  23. package/tests/client/composite/composite.render.test.ripple +45 -13
  24. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  25. package/tests/client/svg.test.ripple +4 -4
  26. package/tests/hydration/basic.test.js +23 -0
  27. package/tests/hydration/compiled/client/basic.js +118 -66
  28. package/tests/hydration/compiled/client/composite.js +90 -37
  29. package/tests/hydration/compiled/client/events.js +18 -18
  30. package/tests/hydration/compiled/client/for.js +62 -62
  31. package/tests/hydration/compiled/client/head.js +10 -10
  32. package/tests/hydration/compiled/client/hmr.js +13 -10
  33. package/tests/hydration/compiled/client/html.js +274 -236
  34. package/tests/hydration/compiled/client/if-children.js +41 -35
  35. package/tests/hydration/compiled/client/if.js +2 -2
  36. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  37. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  38. package/tests/hydration/compiled/client/portal.js +8 -8
  39. package/tests/hydration/compiled/client/reactivity.js +14 -14
  40. package/tests/hydration/compiled/client/return.js +2 -2
  41. package/tests/hydration/compiled/client/try.js +4 -4
  42. package/tests/hydration/compiled/server/basic.js +64 -31
  43. package/tests/hydration/compiled/server/composite.js +62 -29
  44. package/tests/hydration/compiled/server/hmr.js +24 -37
  45. package/tests/hydration/compiled/server/html.js +472 -611
  46. package/tests/hydration/compiled/server/if-children.js +77 -103
  47. package/tests/hydration/compiled/server/portal.js +8 -8
  48. package/tests/hydration/components/basic.ripple +15 -5
  49. package/tests/hydration/components/composite.ripple +13 -1
  50. package/tests/hydration/components/hmr.ripple +1 -3
  51. package/tests/hydration/components/html.ripple +13 -35
  52. package/tests/hydration/components/if-children.ripple +4 -8
  53. package/tests/hydration/composite.test.js +11 -0
  54. package/tests/server/basic.attributes.test.ripple +50 -0
  55. package/tests/server/basic.components.test.ripple +22 -28
  56. package/tests/server/basic.test.ripple +12 -0
  57. package/tests/server/compiler.test.ripple +25 -8
  58. package/tests/server/composite.props.test.ripple +1 -3
  59. package/tests/server/style-identifier.test.ripple +2 -4
  60. package/types/index.d.ts +9 -2
@@ -0,0 +1,39 @@
1
+ const RIPPLE_ELEMENT = Symbol.for('ripple.element');
2
+
3
+ /**
4
+ * @typedef {{
5
+ * render: Function;
6
+ * [RIPPLE_ELEMENT]: true;
7
+ * }} RippleElement
8
+ */
9
+
10
+ /**
11
+ * @param {Function} render
12
+ * @returns {RippleElement}
13
+ */
14
+ export function ripple_element(render) {
15
+ return {
16
+ render,
17
+ [RIPPLE_ELEMENT]: true,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * @param {any} value
23
+ * @returns {value is RippleElement}
24
+ */
25
+ export function is_ripple_element(value) {
26
+ return value != null && value[RIPPLE_ELEMENT] === true;
27
+ }
28
+
29
+ /**
30
+ * @param {any} value
31
+ * @returns {any}
32
+ */
33
+ export function normalize_children(value) {
34
+ if (value == null || is_ripple_element(value) || typeof value !== 'function') {
35
+ return value;
36
+ }
37
+
38
+ return ripple_element(value);
39
+ }
@@ -5,6 +5,7 @@ import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.j
5
5
  import { hydrate_next, hydrating } from './hydration.js';
6
6
  import { active_block, active_namespace, get, with_ns } from './runtime.js';
7
7
  import { top_element_to_ns } from './utils.js';
8
+ import { is_ripple_element } from '../../element.js';
8
9
 
9
10
  /**
10
11
  * @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
@@ -45,13 +46,14 @@ export function composite(get_component, node, props) {
45
46
  });
46
47
  } else if (component != null) {
47
48
  // Custom element - only create if component is not null/undefined
49
+ const ns = top_element_to_ns(component, active_namespace);
48
50
  var run = () => {
49
51
  var block = /** @type {Block} */ (active_block);
50
52
 
51
53
  var element =
52
- active_namespace !== DEFAULT_NAMESPACE
54
+ ns !== DEFAULT_NAMESPACE
53
55
  ? document.createElementNS(
54
- NAMESPACE_URI[active_namespace],
56
+ NAMESPACE_URI[ns],
55
57
  /** @type {keyof HTMLElementTagNameMap} */ (component),
56
58
  )
57
59
  : document.createElement(/** @type {keyof HTMLElementTagNameMap} */ (component));
@@ -67,16 +69,18 @@ export function composite(get_component, node, props) {
67
69
 
68
70
  render_spread(element, () => props || {});
69
71
 
70
- if (typeof props?.children === 'function') {
72
+ if (is_ripple_element(props?.children)) {
71
73
  var child_anchor = document.createComment('');
72
74
  element.appendChild(child_anchor);
73
75
 
74
- props?.children?.(child_anchor, {}, block);
76
+ if (ns !== DEFAULT_NAMESPACE) {
77
+ with_ns(ns, () => props.children.render(child_anchor, {}, block));
78
+ } else {
79
+ props.children.render(child_anchor, {}, block);
80
+ }
75
81
  }
76
82
  };
77
83
 
78
- const ns = top_element_to_ns(component, active_namespace);
79
-
80
84
  if (ns !== active_namespace) {
81
85
  // support top-level dynamic element svg/math <@tag />
82
86
  b = branch(() => with_ns(ns, run));
@@ -0,0 +1,218 @@
1
+ /** @import { Block } from '#client' */
2
+
3
+ import { branch, destroy_block, render } from './blocks.js';
4
+ import { UNINITIALIZED } from './constants.js';
5
+ import { create_text, get_next_sibling } from './operations.js';
6
+ import { active_block } from './runtime.js';
7
+ import { hydrating, set_hydrate_node } from './hydration.js';
8
+ import { COMMENT_NODE, HYDRATION_END, HYDRATION_START, TEXT_NODE } from '../../../constants.js';
9
+ import { is_ripple_element } from '../../element.js';
10
+
11
+ /**
12
+ * @param {Node} node
13
+ * @param {() => any} get_value
14
+ * @returns {void}
15
+ */
16
+ export function expression(node, get_value) {
17
+ var anchor = /** @type {ChildNode} */ (node);
18
+ /** @type {Block | null} */
19
+ var child_block = null;
20
+ /** @type {Comment | null} */
21
+ var end = null;
22
+ /** @type {Text | null} */
23
+ var text = null;
24
+ /** @type {string | import('../../element.js').RippleElement | typeof UNINITIALIZED} */
25
+ var value = UNINITIALIZED;
26
+ var is_element = false;
27
+ var initialized = false;
28
+
29
+ render(() => {
30
+ var next_value = get_value();
31
+ var next_is_element = is_ripple_element(next_value);
32
+ var is_hydration_marker = hydrating && anchor.nodeType === COMMENT_NODE;
33
+
34
+ if (is_hydration_marker) {
35
+ end ??= ensure_expression_end(anchor);
36
+ }
37
+
38
+ if (next_is_element) {
39
+ if (initialized && is_element && value === next_value) {
40
+ if (end !== null) {
41
+ advance_hydration(end);
42
+ }
43
+ return;
44
+ }
45
+
46
+ if (anchor.nodeType === TEXT_NODE) {
47
+ /** @type {Text} */ (anchor).nodeValue = '';
48
+ } else if (text !== null) {
49
+ text.remove();
50
+ text = null;
51
+ }
52
+
53
+ if (child_block !== null) {
54
+ destroy_block(child_block);
55
+ child_block = null;
56
+ }
57
+
58
+ if (end !== null && (initialized || !hydrating)) {
59
+ clear_expression_range(anchor, end);
60
+ }
61
+
62
+ if (is_hydration_marker) {
63
+ set_hydrate_node(get_next_sibling(anchor) ?? end);
64
+ }
65
+
66
+ child_block = branch(() => {
67
+ var block = active_block;
68
+ next_value.render(end ?? anchor, {}, block);
69
+ });
70
+
71
+ value = next_value;
72
+ is_element = true;
73
+ initialized = true;
74
+ if (end !== null) {
75
+ advance_hydration(end);
76
+ }
77
+ return;
78
+ }
79
+
80
+ var next_text = (next_value ?? '') + '';
81
+
82
+ if (initialized && !is_element && value === next_text) {
83
+ if (end !== null) {
84
+ advance_hydration(end);
85
+ }
86
+ return;
87
+ }
88
+
89
+ if (child_block !== null) {
90
+ destroy_block(child_block);
91
+ child_block = null;
92
+ }
93
+
94
+ if (is_hydration_marker) {
95
+ text = get_hydrated_text(anchor, /** @type {Comment} */ (end));
96
+
97
+ if (next_text === '') {
98
+ if (text !== null) {
99
+ text.remove();
100
+ text = null;
101
+ }
102
+ } else if (text === null) {
103
+ text = create_text(next_text);
104
+ /** @type {Comment} */ (end).before(text);
105
+ } else if (text.nodeValue !== next_text) {
106
+ text.nodeValue = next_text;
107
+ }
108
+ } else if (anchor.nodeType === COMMENT_NODE) {
109
+ if (next_text === '') {
110
+ if (text !== null) {
111
+ text.remove();
112
+ text = null;
113
+ }
114
+ } else if (text === null) {
115
+ text = create_text(next_text);
116
+ (end ?? anchor).before(text);
117
+ } else if (text.nodeValue !== next_text) {
118
+ text.nodeValue = next_text;
119
+ }
120
+ } else if (anchor.nodeType === TEXT_NODE) {
121
+ /** @type {Text} */ (anchor).nodeValue = next_text;
122
+ }
123
+
124
+ value = next_text;
125
+ is_element = false;
126
+ initialized = true;
127
+ if (end !== null) {
128
+ advance_hydration(end);
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * @param {Node} anchor
135
+ * @returns {Comment}
136
+ */
137
+ function ensure_expression_end(anchor) {
138
+ if (hydrating) {
139
+ /** @type {Node | null} */
140
+ var current = get_next_sibling(anchor);
141
+ var depth = 0;
142
+
143
+ while (current !== null) {
144
+ if (current.nodeType === COMMENT_NODE) {
145
+ var data = /** @type {Comment} */ (current).data;
146
+
147
+ if (data === HYDRATION_START) {
148
+ depth += 1;
149
+ } else if (data === HYDRATION_END) {
150
+ if (depth === 0) {
151
+ return /** @type {Comment} */ (current);
152
+ }
153
+
154
+ depth -= 1;
155
+ }
156
+ }
157
+
158
+ current = get_next_sibling(current);
159
+ }
160
+
161
+ throw new Error('Hydration mismatch: expected end marker for expression block');
162
+ }
163
+
164
+ var end = document.createComment(HYDRATION_END);
165
+ /** @type {ChildNode} */ (anchor).after(end);
166
+ return end;
167
+ }
168
+
169
+ /**
170
+ * @param {Node} anchor
171
+ * @param {Node} end
172
+ * @returns {Text | null}
173
+ */
174
+ function get_hydrated_text(anchor, end) {
175
+ var first = get_next_sibling(anchor);
176
+
177
+ if (first === end) {
178
+ return null;
179
+ }
180
+
181
+ if (first?.nodeType === TEXT_NODE && get_next_sibling(first) === end) {
182
+ return /** @type {Text} */ (first);
183
+ }
184
+
185
+ clear_expression_range(anchor, end);
186
+ return null;
187
+ }
188
+
189
+ /**
190
+ * @param {Node} anchor
191
+ * @param {Node} end
192
+ * @returns {void}
193
+ */
194
+ function clear_expression_range(anchor, end) {
195
+ var current = get_next_sibling(anchor);
196
+
197
+ while (current !== null && current !== end) {
198
+ var next = get_next_sibling(current);
199
+ /** @type {ChildNode} */ (current).remove();
200
+ current = next;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @param {Comment} end
206
+ * @returns {void}
207
+ */
208
+ function advance_hydration(end) {
209
+ if (!hydrating) {
210
+ return;
211
+ }
212
+
213
+ var next = get_next_sibling(end);
214
+
215
+ if (next !== null) {
216
+ set_hydrate_node(next);
217
+ }
218
+ }
@@ -105,6 +105,8 @@ export { script } from './script.js';
105
105
 
106
106
  export { html } from './html.js';
107
107
 
108
+ export { expression } from './expression.js';
109
+
108
110
  export { rpc } from './rpc.js';
109
111
 
110
112
  export { tsx_compat } from './compat.js';
@@ -114,3 +116,5 @@ export { TRY_BLOCK, HMR } from './constants.js';
114
116
  export { hmr } from './hmr.js';
115
117
 
116
118
  export { pop, next } from './hydration.js';
119
+
120
+ export { ripple_element, normalize_children } from '../../element.js';
@@ -12,10 +12,11 @@ import {
12
12
  set_hydrating,
13
13
  set_hydrate_node,
14
14
  } from './hydration.js';
15
+ import { is_ripple_element } from '../../element.js';
15
16
 
16
17
  /**
17
18
  * @param {any} _
18
- * @param {{ target: Element, children: (anchor: Node, props: {}, block: Block) => void }} props
19
+ * @param {{ target: Element, children: import('../../element.js').RippleElement }} props
19
20
  * @returns {void}
20
21
  */
21
22
  export function Portal(_, props) {
@@ -26,7 +27,7 @@ export function Portal(_, props) {
26
27
 
27
28
  /** @type {Element | symbol} */
28
29
  let target = UNINITIALIZED;
29
- /** @type {((anchor: Node, props: {}, block: Block) => void) | symbol} */
30
+ /** @type {import('../../element.js').RippleElement | symbol} */
30
31
  let children = UNINITIALIZED;
31
32
  /** @type {Block | null} */
32
33
  var b = null;
@@ -44,8 +45,13 @@ export function Portal(_, props) {
44
45
 
45
46
  try {
46
47
  render(() => {
47
- if (target === (target = props.target)) return;
48
- if (children === (children = props.children)) return;
48
+ const next_target = props.target;
49
+ const next_children = props.children;
50
+
51
+ if (target === next_target && children === next_children) return;
52
+
53
+ target = next_target;
54
+ children = next_children;
49
55
 
50
56
  if (b !== null) {
51
57
  destroy_block(b);
@@ -65,8 +71,8 @@ export function Portal(_, props) {
65
71
  var block = /** @type {Block} */ (active_block);
66
72
 
67
73
  b = branch(() => {
68
- if (typeof children === 'function') {
69
- children(/** @type {Text} */ (anchor), {}, block);
74
+ if (is_ripple_element(children)) {
75
+ children.render(/** @type {Text} */ (anchor), {}, block);
70
76
  }
71
77
  });
72
78
 
@@ -17,6 +17,7 @@ import { is_boolean_attribute } from '../../../compiler/utils.js';
17
17
  import { clsx } from 'clsx';
18
18
  import { normalize_css_property_name } from '../../../utils/normalize_css_property_name.js';
19
19
  import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
20
+ import { is_ripple_element, normalize_children, ripple_element } from '../../element.js';
20
21
  import {
21
22
  is_tag_valid_with_parent,
22
23
  is_tag_valid_with_ancestor,
@@ -27,6 +28,30 @@ export { register_component_css as register_css } from './css-registry.js';
27
28
  export { hash } from '../../../utils/hashing.js';
28
29
  export { context } from './context.js';
29
30
  export { array_slice };
31
+ export { ripple_element, normalize_children };
32
+
33
+ /**
34
+ * @param {Output} output
35
+ * @param {any} value
36
+ * @returns {void}
37
+ */
38
+ export function render_expression(output, value) {
39
+ output.push(BLOCK_OPEN);
40
+
41
+ if (is_ripple_element(value)) {
42
+ var result = value.render(output, {});
43
+
44
+ if (result && typeof result.then === 'function') {
45
+ return result.then(() => {
46
+ output.push(BLOCK_CLOSE);
47
+ });
48
+ }
49
+ } else {
50
+ output.push(escape(value ?? ''));
51
+ }
52
+
53
+ output.push(BLOCK_CLOSE);
54
+ }
30
55
 
31
56
  /** @type {null | Component} */
32
57
  export let active_component = null;
@@ -596,7 +621,7 @@ export function spread_attrs(attrs, css_hash) {
596
621
  for (name in attrs) {
597
622
  var value = attrs[name];
598
623
 
599
- if (typeof value === 'function') continue;
624
+ if (name === 'children' || typeof value === 'function' || is_ripple_element(value)) continue;
600
625
 
601
626
  if (is_ripple_object(value)) {
602
627
  value = get(value);
@@ -7,21 +7,20 @@ import type {
7
7
  PropsWithChildrenOptional,
8
8
  } from 'ripple';
9
9
  import { flushSync, track } from 'ripple';
10
+ import { did_error } from '../capture-error.js';
10
11
 
11
12
  describe('basic client > components & composition', () => {
12
13
  it('renders with component composition and children', () => {
13
14
  component Card(props: PropsWithChildren<{}>) {
14
- <div class="card">
15
- <props.children />
16
- </div>
15
+ <div class="card">{props.children}</div>
17
16
  }
18
17
 
19
18
  component Basic() {
20
- <Card>
21
- component children() {
22
- <p>{'Card content here'}</p>
23
- }
24
- </Card>
19
+ component children() {
20
+ <p>{'Card content here'}</p>
21
+ }
22
+
23
+ <Card {children} />
25
24
  }
26
25
 
27
26
  render(Basic);
@@ -37,17 +36,17 @@ describe('basic client > components & composition', () => {
37
36
  component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
38
37
  <div class="card">
39
38
  if (props.children) {
40
- <props.children />
39
+ {props.children}
41
40
  }
42
41
  </div>
43
42
  }
44
43
 
45
44
  component Basic() {
46
- <Card>
47
- component test() {
48
- <p>{'Card content here'}</p>
49
- }
50
- </Card>
45
+ component test() {
46
+ <p>{'Card content here'}</p>
47
+ }
48
+
49
+ <Card {test} />
51
50
  }
52
51
 
53
52
  render(Basic);
@@ -59,22 +58,23 @@ describe('basic client > components & composition', () => {
59
58
  expect(paragraph).toBeFalsy();
60
59
  });
61
60
 
62
- it('allows tracked variable and slot component with same name in nested scope', () => {
61
+ it('allows tracked variables alongside explicit component props', () => {
63
62
  component Card(props: PropsWithChildrenOptional<{ test?: Component }>) {
64
63
  <div class="card">
65
64
  if (props.children) {
66
- <props.children />
65
+ {props.children}
67
66
  }
68
67
  </div>
69
68
  }
70
69
 
71
70
  component Basic() {
72
71
  let &[test] = track(false);
73
- <Card>
74
- component test() {
75
- <p>{'Card content here'}</p>
76
- }
77
- </Card>
72
+
73
+ component TestSlot() {
74
+ <p>{'Card content here'}</p>
75
+ }
76
+
77
+ <Card test={TestSlot} />
78
78
  <div>{test ? 'yes' : 'no'}</div>
79
79
  }
80
80
 
@@ -90,9 +90,7 @@ describe('basic client > components & composition', () => {
90
90
 
91
91
  it('renders a component when children is set a component prop', () => {
92
92
  component Card(props: PropsWithChildren<{}>) {
93
- <div class="card">
94
- <props.children />
95
- </div>
93
+ <div class="card">{props.children}</div>
96
94
  }
97
95
 
98
96
  component Basic() {
@@ -211,6 +209,28 @@ describe('basic client > components & composition', () => {
211
209
  expect(countDiv.textContent).toBe('3');
212
210
  });
213
211
 
212
+ it('updates explicit text children props reactively', () => {
213
+ component TextProp(&{ children }: PropsWithChildren<{}>) {
214
+ <div class="text-prop">{children}</div>
215
+ }
216
+
217
+ component Basic() {
218
+ let &[show] = track(false);
219
+
220
+ <TextProp children={show ? 'hello' : ''} />
221
+ <button class="show-text" onClick={() => (show = true)}>{'Show'}</button>
222
+ }
223
+
224
+ render(Basic);
225
+
226
+ expect(container.querySelector('.text-prop')?.textContent).toBe('');
227
+
228
+ container.querySelector('.show-text')?.click();
229
+ flushSync();
230
+
231
+ expect(container.querySelector('.text-prop')?.textContent).toBe('hello');
232
+ });
233
+
214
234
  it('it retains this context with bracketed prop functions and keeps original chaining', () => {
215
235
  component App() {
216
236
  const SYMBOL_PROP = Symbol();
@@ -233,60 +253,40 @@ describe('basic client > components & composition', () => {
233
253
 
234
254
  const obj2 = null;
235
255
 
256
+ function trigger_nonexistent() {
257
+ hasError = did_error(() => {
258
+ // @ts-ignore
259
+ obj['nonexistent']();
260
+ });
261
+ }
262
+
263
+ function trigger_nonexistent_chaining() {
264
+ hasError = did_error(() => {
265
+ // @ts-ignore
266
+ obj['nonexistent']?.();
267
+ });
268
+ }
269
+
270
+ function trigger_object_null() {
271
+ hasError = did_error(() => {
272
+ // @ts-ignore
273
+ obj2['nonexistent']();
274
+ });
275
+ }
276
+
277
+ function trigger_object_null_chained() {
278
+ hasError = did_error(() => {
279
+ // @ts-ignore
280
+ obj2?.['nonexistent']?.();
281
+ });
282
+ }
283
+
236
284
  <button onClick={() => obj['increment']()}>{'Increment'}</button>
237
285
  <button onClick={() => obj[SYMBOL_PROP]()}>{'Increment'}</button>
238
- <button
239
- onClick={() => {
240
- hasError = false;
241
- try {
242
- // @ts-ignore
243
- obj['nonexistent']();
244
- } catch {
245
- hasError = true;
246
- }
247
- }}
248
- >
249
- {'Nonexistent'}
250
- </button>
251
- <button
252
- onClick={() => {
253
- hasError = false;
254
- try {
255
- // @ts-ignore
256
- obj['nonexistent']?.();
257
- } catch {
258
- hasError = true;
259
- }
260
- }}
261
- >
262
- {'Nonexistent chaining'}
263
- </button>
264
- <button
265
- onClick={() => {
266
- hasError = false;
267
- try {
268
- // @ts-ignore
269
- obj2['nonexistent']();
270
- } catch {
271
- hasError = true;
272
- }
273
- }}
274
- >
275
- {'Object null'}
276
- </button>
277
- <button
278
- onClick={() => {
279
- hasError = false;
280
- try {
281
- // @ts-ignore
282
- obj2?.['nonexistent']?.();
283
- } catch {
284
- hasError = true;
285
- }
286
- }}
287
- >
288
- {'Object null chained'}
289
- </button>
286
+ <button onClick={trigger_nonexistent}>{'Nonexistent'}</button>
287
+ <button onClick={trigger_nonexistent_chaining}>{'Nonexistent chaining'}</button>
288
+ <button onClick={trigger_object_null}>{'Object null'}</button>
289
+ <button onClick={trigger_object_null_chained}>{'Object null chained'}</button>
290
290
  <button onClick={() => obj.arr[obj.arr.length - 1]()}>{'BinaryExpression prop'}</button>
291
291
 
292
292
  <span>{obj.count.value}</span>
@@ -346,21 +346,19 @@ describe('basic client > components & composition', () => {
346
346
  <span>{'Hello from Span'}</span>
347
347
  },
348
348
  button: component({ children }: PropsWithChildren<{}>) {
349
- <button>
350
- <children />
351
- </button>
349
+ <button>{children}</button>
352
350
  },
353
351
  };
354
352
 
355
353
  component App() {
354
+ component children() {
355
+ <span>{'Click me!'}</span>
356
+ }
357
+
356
358
  <div>
357
359
  <h1>{'Component as Property Test'}</h1>
358
360
  <UI.span />
359
- <UI.button>
360
- component children() {
361
- <span>{'Click me!'}</span>
362
- }
363
- </UI.button>
361
+ <UI.button {children} />
364
362
  </div>
365
363
  }
366
364
 
@@ -378,13 +376,13 @@ describe('basic client > components & composition', () => {
378
376
 
379
377
  it('handles empty string children', () => {
380
378
  component Button({ children }: PropsWithChildren<{}>) {
381
- <children />
379
+ {children}
382
380
  }
383
381
 
384
382
  component App() {
385
- let text = '';
383
+ let content = '';
386
384
  <Button>{''}</Button>
387
- <Button>{text}</Button>
385
+ <Button>{content}</Button>
388
386
  }
389
387
 
390
388
  expect(() => {