ripple 0.3.8 → 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 (79) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +38 -172
  4. package/src/compiler/phases/2-analyze/index.js +308 -115
  5. package/src/compiler/phases/2-analyze/prune.js +13 -5
  6. package/src/compiler/phases/3-transform/client/index.js +197 -213
  7. package/src/compiler/phases/3-transform/segments.js +0 -7
  8. package/src/compiler/phases/3-transform/server/index.js +77 -170
  9. package/src/compiler/types/acorn.d.ts +1 -1
  10. package/src/compiler/types/estree.d.ts +1 -1
  11. package/src/compiler/types/import.d.ts +0 -2
  12. package/src/compiler/types/index.d.ts +14 -18
  13. package/src/compiler/types/parse.d.ts +3 -9
  14. package/src/compiler/utils.js +154 -21
  15. package/src/runtime/element.js +39 -0
  16. package/src/runtime/index-client.js +2 -13
  17. package/src/runtime/index-server.js +2 -2
  18. package/src/runtime/internal/client/bindings.js +3 -1
  19. package/src/runtime/internal/client/composite.js +11 -6
  20. package/src/runtime/internal/client/events.js +1 -1
  21. package/src/runtime/internal/client/expression.js +218 -0
  22. package/src/runtime/internal/client/head.js +3 -4
  23. package/src/runtime/internal/client/index.js +4 -1
  24. package/src/runtime/internal/client/portal.js +12 -6
  25. package/src/runtime/internal/client/runtime.js +0 -52
  26. package/src/runtime/internal/server/index.js +57 -56
  27. package/tests/client/basic/basic.components.test.ripple +85 -87
  28. package/tests/client/basic/basic.errors.test.ripple +28 -4
  29. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  30. package/tests/client/basic/basic.rendering.test.ripple +23 -8
  31. package/tests/client/capture-error.js +12 -0
  32. package/tests/client/compiler/compiler.basic.test.ripple +107 -18
  33. package/tests/client/composite/composite.props.test.ripple +5 -9
  34. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  35. package/tests/client/composite/composite.render.test.ripple +45 -13
  36. package/tests/client/css/global-additional-cases.test.ripple +3 -3
  37. package/tests/client/dynamic-elements.test.ripple +3 -4
  38. package/tests/client/lazy-destructuring.test.ripple +69 -12
  39. package/tests/client/svg.test.ripple +4 -4
  40. package/tests/hydration/basic.test.js +23 -0
  41. package/tests/hydration/compiled/client/basic.js +118 -66
  42. package/tests/hydration/compiled/client/composite.js +90 -37
  43. package/tests/hydration/compiled/client/events.js +18 -18
  44. package/tests/hydration/compiled/client/for.js +62 -62
  45. package/tests/hydration/compiled/client/head.js +10 -10
  46. package/tests/hydration/compiled/client/hmr.js +13 -10
  47. package/tests/hydration/compiled/client/html.js +274 -236
  48. package/tests/hydration/compiled/client/if-children.js +41 -35
  49. package/tests/hydration/compiled/client/if.js +2 -2
  50. package/tests/hydration/compiled/client/mixed-control-flow.js +12 -12
  51. package/tests/hydration/compiled/client/nested-control-flow.js +46 -46
  52. package/tests/hydration/compiled/client/portal.js +8 -8
  53. package/tests/hydration/compiled/client/reactivity.js +14 -14
  54. package/tests/hydration/compiled/client/return.js +2 -2
  55. package/tests/hydration/compiled/client/try.js +4 -4
  56. package/tests/hydration/compiled/server/basic.js +64 -31
  57. package/tests/hydration/compiled/server/composite.js +62 -29
  58. package/tests/hydration/compiled/server/hmr.js +24 -37
  59. package/tests/hydration/compiled/server/html.js +472 -611
  60. package/tests/hydration/compiled/server/if-children.js +77 -103
  61. package/tests/hydration/compiled/server/portal.js +8 -8
  62. package/tests/hydration/components/basic.ripple +15 -5
  63. package/tests/hydration/components/composite.ripple +13 -1
  64. package/tests/hydration/components/hmr.ripple +1 -3
  65. package/tests/hydration/components/html.ripple +13 -35
  66. package/tests/hydration/components/if-children.ripple +4 -8
  67. package/tests/hydration/composite.test.js +11 -0
  68. package/tests/server/basic.attributes.test.ripple +50 -0
  69. package/tests/server/basic.components.test.ripple +22 -28
  70. package/tests/server/basic.test.ripple +12 -0
  71. package/tests/server/compiler.test.ripple +43 -4
  72. package/tests/server/composite.props.test.ripple +5 -9
  73. package/tests/server/dynamic-elements.test.ripple +3 -4
  74. package/tests/server/lazy-destructuring.test.ripple +68 -12
  75. package/tests/server/style-identifier.test.ripple +2 -4
  76. package/tsconfig.typecheck.json +4 -0
  77. package/types/index.d.ts +9 -21
  78. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  79. package/tests/client/tracked-expression.test.ripple +0 -26
@@ -426,58 +426,6 @@ export function track(v, get, set, b) {
426
426
  return tracked(v, b, get, set);
427
427
  }
428
428
 
429
- /**
430
- * @param {Record<string|symbol, any>} v
431
- * @param {(symbol | string)[]} l
432
- * @param {Block} b
433
- * @returns {Tracked[]}
434
- */
435
- export function track_split(v, l, b) {
436
- var is_tracked = is_ripple_object(v);
437
-
438
- if (is_tracked || typeof v !== 'object' || v === null || is_array(v)) {
439
- throw new TypeError('Invalid value: expected a non-tracked object');
440
- }
441
-
442
- /** @type {Tracked[]} */
443
- var out = [];
444
- /** @type {Record<string|symbol, any>} */
445
- var rest = {};
446
- /** @type {Record<PropertyKey, 1>} */
447
- var done = {};
448
- var props = Reflect.ownKeys(v);
449
-
450
- for (let i = 0, key, t; i < l.length; i++) {
451
- key = l[i];
452
-
453
- if (props.includes(key)) {
454
- if (is_ripple_object(v[key])) {
455
- t = v[key];
456
- } else {
457
- t = tracked(undefined, b);
458
- t = define_property(t, '__v', /** @type {PropertyDescriptor} */ (get_descriptor(v, key)));
459
- }
460
- } else {
461
- t = tracked(undefined, b);
462
- }
463
-
464
- out[i] = t;
465
- done[key] = 1;
466
- }
467
-
468
- for (let i = 0, key; i < props.length; i++) {
469
- key = props[i];
470
- if (done[key]) {
471
- continue;
472
- }
473
- define_property(rest, key, /** @type {PropertyDescriptor} */ (get_descriptor(v, key)));
474
- }
475
-
476
- out.push(tracked(rest, b));
477
-
478
- return out;
479
- }
480
-
481
429
  /**
482
430
  * @param {Tracked | Derived} tracked
483
431
  * @returns {Dependency}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  @import { Component, Dependency, Derived, Tracked } from '#server';
3
- @import { SSRComponent, renderToStream, render } from 'ripple/server';
3
+ @import { SSRComponent } from 'ripple/server';
4
4
  */
5
5
 
6
6
  import { Readable } from 'stream';
@@ -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;
@@ -231,12 +256,12 @@ class Output {
231
256
  }
232
257
  }
233
258
 
234
- /** @type {render} */
259
+ /** @type {import('ripple/server').render} */
235
260
  export async function render(component) {
236
261
  const output = new Output(null, null);
237
262
  let head = '';
238
263
  let body = '';
239
- let css = new Set();
264
+ let css = /** @type {Set<string>} */ (new Set());
240
265
 
241
266
  // Reset dev-mode element tracking state at the start of each render
242
267
  reset_element_state();
@@ -262,7 +287,7 @@ export async function render(component) {
262
287
  return { head, body, css };
263
288
  }
264
289
 
265
- /** @type {renderToStream} */
290
+ /** @type {import('ripple/server').renderToStream} */
266
291
  export function renderToStream(component) {
267
292
  const stream = new Readable({
268
293
  read() {},
@@ -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);
@@ -702,6 +727,24 @@ function tracked(v, get, set) {
702
727
  return /** @type {Tracked} */ (new TrackedValue(v, get || set ? { get, set } : empty_get_set));
703
728
  }
704
729
 
730
+ /**
731
+ * @param {Record<string, unknown>} obj
732
+ * @param {string[]} exclude_keys
733
+ * @returns {Record<string, unknown>}
734
+ */
735
+ export function exclude_from_object(obj, exclude_keys) {
736
+ /** @type {Record<string, unknown>} */
737
+ var new_obj = {};
738
+
739
+ for (const key of Object.keys(obj)) {
740
+ if (!exclude_keys.includes(key)) {
741
+ new_obj[key] = obj[key];
742
+ }
743
+ }
744
+
745
+ return new_obj;
746
+ }
747
+
705
748
  /**
706
749
  * @param {any} v
707
750
  * @param {(value: any) => any} [get]
@@ -722,57 +765,6 @@ export function track(v, get, set) {
722
765
  return tracked(v, get, set);
723
766
  }
724
767
 
725
- /**
726
- * @param {Record<string|symbol, any>} v
727
- * @param {(symbol | string)[]} l
728
- * @returns {Tracked[]}
729
- */
730
- export function track_split(v, l) {
731
- var is_tracked = is_ripple_object(v);
732
-
733
- if (is_tracked || typeof v !== 'object' || v === null || is_array(v)) {
734
- throw new TypeError('Invalid value: expected a non-tracked object');
735
- }
736
-
737
- /** @type {Tracked[]} */
738
- var out = [];
739
- /** @type {Record<string|symbol, any>} */
740
- var rest = {};
741
- /** @type {Record<PropertyKey, 1>} */
742
- var done = {};
743
- var props = Reflect.ownKeys(v);
744
-
745
- for (let i = 0, key, t; i < l.length; i++) {
746
- key = l[i];
747
-
748
- if (props.includes(key)) {
749
- if (is_ripple_object(v[key])) {
750
- t = v[key];
751
- } else {
752
- t = tracked(undefined);
753
- t = define_property(t, 'v', /** @type {PropertyDescriptor} */ (get_descriptor(v, key)));
754
- }
755
- } else {
756
- t = tracked(undefined);
757
- }
758
-
759
- out[i] = t;
760
- done[key] = 1;
761
- }
762
-
763
- for (let i = 0, key; i < props.length; i++) {
764
- key = props[i];
765
- if (done[key]) {
766
- continue;
767
- }
768
- define_property(rest, key, /** @type {PropertyDescriptor} */ (get_descriptor(v, key)));
769
- }
770
-
771
- out.push(tracked(rest));
772
-
773
- return out;
774
- }
775
-
776
768
  /**
777
769
  * @param {any} _
778
770
  * @param {ConstructorParameters<typeof URL>} params
@@ -865,6 +857,15 @@ export function ripple_object(obj) {
865
857
  return obj;
866
858
  }
867
859
 
860
+ /**
861
+ * @template K, V
862
+ * @param {Iterable<readonly [K, V]>} [iterable]
863
+ * @returns {Map<K, V>}
864
+ */
865
+ export function ripple_map(iterable) {
866
+ return new Map(iterable);
867
+ }
868
+
868
869
  /**
869
870
  * Returns the fallback value if the given value is undefined.
870
871
  * @template T
@@ -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(() => {
@@ -70,9 +70,7 @@ describe('basic client > errors', () => {
70
70
 
71
71
  expect(() => {
72
72
  compile(code, 'test.ripple');
73
- }).toThrow(
74
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
75
- );
73
+ }).not.toThrow();
76
74
  });
77
75
 
78
76
  it('should throw error for interpolating props.children as text', () => {
@@ -82,10 +80,36 @@ describe('basic client > errors', () => {
82
80
  }
83
81
  `;
84
82
 
83
+ expect(() => {
84
+ compile(code, 'test.ripple');
85
+ }).not.toThrow();
86
+ });
87
+
88
+ it('should throw error for calling children as a function', () => {
89
+ const code = `
90
+ export component Layout({ children }) {
91
+ {children()}
92
+ }
93
+ `;
94
+
95
+ expect(() => {
96
+ compile(code, 'test.ripple');
97
+ }).toThrow(
98
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
99
+ );
100
+ });
101
+
102
+ it('should throw error for calling props.children as a function', () => {
103
+ const code = `
104
+ export component Layout(props) {
105
+ {props.children()}
106
+ }
107
+ `;
108
+
85
109
  expect(() => {
86
110
  compile(code, 'test.ripple');
87
111
  }).toThrow(
88
- '`children` cannot be rendered using text interpolation. Use `<children />` instead.',
112
+ '`children` cannot be called like a regular function. Render it with `{children}` or `{props.children}` instead.',
89
113
  );
90
114
  });
91
115