ripple 0.3.8 → 0.3.9

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 (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  3. package/src/compiler/phases/1-parse/index.js +13 -157
  4. package/src/compiler/phases/2-analyze/index.js +289 -43
  5. package/src/compiler/phases/3-transform/client/index.js +9 -157
  6. package/src/compiler/phases/3-transform/segments.js +0 -7
  7. package/src/compiler/phases/3-transform/server/index.js +15 -130
  8. package/src/compiler/types/acorn.d.ts +1 -1
  9. package/src/compiler/types/estree.d.ts +1 -1
  10. package/src/compiler/types/import.d.ts +0 -2
  11. package/src/compiler/types/index.d.ts +5 -17
  12. package/src/compiler/types/parse.d.ts +1 -9
  13. package/src/compiler/utils.js +53 -20
  14. package/src/runtime/index-client.js +2 -13
  15. package/src/runtime/index-server.js +2 -2
  16. package/src/runtime/internal/client/bindings.js +3 -1
  17. package/src/runtime/internal/client/composite.js +1 -0
  18. package/src/runtime/internal/client/events.js +1 -1
  19. package/src/runtime/internal/client/head.js +3 -4
  20. package/src/runtime/internal/client/index.js +0 -1
  21. package/src/runtime/internal/client/runtime.js +0 -52
  22. package/src/runtime/internal/server/index.js +31 -55
  23. package/tests/client/basic/basic.errors.test.ripple +28 -0
  24. package/tests/client/basic/basic.reactivity.test.ripple +10 -155
  25. package/tests/client/compiler/compiler.basic.test.ripple +31 -12
  26. package/tests/client/composite/composite.props.test.ripple +5 -7
  27. package/tests/client/composite/composite.reactivity.test.ripple +35 -36
  28. package/tests/client/dynamic-elements.test.ripple +3 -4
  29. package/tests/client/lazy-destructuring.test.ripple +69 -12
  30. package/tests/server/compiler.test.ripple +22 -0
  31. package/tests/server/composite.props.test.ripple +5 -7
  32. package/tests/server/dynamic-elements.test.ripple +3 -4
  33. package/tests/server/lazy-destructuring.test.ripple +68 -12
  34. package/tsconfig.typecheck.json +4 -0
  35. package/types/index.d.ts +0 -19
  36. package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +0 -34
  37. package/tests/client/tracked-expression.test.ripple +0 -26
@@ -102,13 +102,7 @@ export function hydrate(component, options) {
102
102
 
103
103
  export { Context } from './internal/client/context.js';
104
104
 
105
- export {
106
- flush_sync as flushSync,
107
- track,
108
- track_split as trackSplit,
109
- untrack,
110
- tick,
111
- } from './internal/client/runtime.js';
105
+ export { flush_sync as flushSync, track, untrack, tick } from './internal/client/runtime.js';
112
106
 
113
107
  export { RippleArray } from './array.js';
114
108
 
@@ -165,10 +159,5 @@ import { RippleURL } from './url.js';
165
159
  import { RippleURLSearchParams } from './url-search-params.js';
166
160
  import { RippleDate } from './date.js';
167
161
  import { MediaQuery } from './media-query.js';
168
- import {
169
- track,
170
- track_split as trackSplit,
171
- untrack,
172
- ref_prop as createRefKey,
173
- } from './internal/client/runtime.js';
162
+ import { track, untrack, ref_prop as createRefKey } from './internal/client/runtime.js';
174
163
  import { user_effect as effect } from './internal/client/blocks.js';
@@ -1,8 +1,8 @@
1
- import { get, set, untrack, track, track_split } from './internal/server/index.js';
1
+ import { get, set, untrack, track } from './internal/server/index.js';
2
2
 
3
3
  export { Context } from './internal/server/context.js';
4
4
 
5
- export { get, set, untrack, track, track_split as trackSplit };
5
+ export { get, set, untrack, track };
6
6
 
7
7
  function noop() {}
8
8
 
@@ -186,7 +186,9 @@ function select_option(select, value, mounting = false) {
186
186
 
187
187
  // Otherwise, update the selection
188
188
  for (var option of select.options) {
189
- option.selected = /** @type {string[]} */ (value).includes(get_option_value(option));
189
+ option.selected = /** @type {string[]} */ (value).includes(
190
+ /** @type {string} */ (get_option_value(option)),
191
+ );
190
192
  }
191
193
 
192
194
  return;
@@ -29,6 +29,7 @@ export function composite(get_component, node, props) {
29
29
 
30
30
  render(
31
31
  () => {
32
+ // @ts-ignore — get() handles non-tracked values via is_ripple_object() check
32
33
  var component = get(get_component());
33
34
 
34
35
  if (b !== null) {
@@ -168,7 +168,7 @@ export function handle_event_propagation(event) {
168
168
  null;
169
169
 
170
170
  try {
171
- var delegated = current_target['__' + event_name];
171
+ var delegated = /** @type {Record<string, any>} */ (current_target)['__' + event_name];
172
172
 
173
173
  if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
174
174
  if (is_array(delegated)) {
@@ -1,4 +1,3 @@
1
- /** @import { TemplateNode } from '#client' */
2
1
  import { render } from './blocks.js';
3
2
  import { HEAD_BLOCK } from './constants.js';
4
3
  import { COMMENT_NODE } from '../../../constants.js';
@@ -38,8 +37,8 @@ export function head(hash, render_fn) {
38
37
  if (head_anchor === null) {
39
38
  set_hydrating(false);
40
39
  } else {
41
- var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
42
- head_anchor.remove(); // in case this component is repeated
40
+ var start = get_next_sibling(head_anchor);
41
+ /** @type {ChildNode} */ (head_anchor).remove(); // in case this component is repeated
43
42
 
44
43
  set_hydrate_node(start);
45
44
  }
@@ -54,7 +53,7 @@ export function head(hash, render_fn) {
54
53
  } finally {
55
54
  if (was_hydrating) {
56
55
  set_hydrating(true);
57
- set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
56
+ set_hydrate_node(previous_hydrate_node);
58
57
  }
59
58
  }
60
59
  }
@@ -52,7 +52,6 @@ export {
52
52
  update_property,
53
53
  update_pre_property,
54
54
  track,
55
- track_split,
56
55
  push_component,
57
56
  pop_component,
58
57
  untrack,
@@ -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';
@@ -231,12 +231,12 @@ class Output {
231
231
  }
232
232
  }
233
233
 
234
- /** @type {render} */
234
+ /** @type {import('ripple/server').render} */
235
235
  export async function render(component) {
236
236
  const output = new Output(null, null);
237
237
  let head = '';
238
238
  let body = '';
239
- let css = new Set();
239
+ let css = /** @type {Set<string>} */ (new Set());
240
240
 
241
241
  // Reset dev-mode element tracking state at the start of each render
242
242
  reset_element_state();
@@ -262,7 +262,7 @@ export async function render(component) {
262
262
  return { head, body, css };
263
263
  }
264
264
 
265
- /** @type {renderToStream} */
265
+ /** @type {import('ripple/server').renderToStream} */
266
266
  export function renderToStream(component) {
267
267
  const stream = new Readable({
268
268
  read() {},
@@ -702,6 +702,24 @@ function tracked(v, get, set) {
702
702
  return /** @type {Tracked} */ (new TrackedValue(v, get || set ? { get, set } : empty_get_set));
703
703
  }
704
704
 
705
+ /**
706
+ * @param {Record<string, unknown>} obj
707
+ * @param {string[]} exclude_keys
708
+ * @returns {Record<string, unknown>}
709
+ */
710
+ export function exclude_from_object(obj, exclude_keys) {
711
+ /** @type {Record<string, unknown>} */
712
+ var new_obj = {};
713
+
714
+ for (const key of Object.keys(obj)) {
715
+ if (!exclude_keys.includes(key)) {
716
+ new_obj[key] = obj[key];
717
+ }
718
+ }
719
+
720
+ return new_obj;
721
+ }
722
+
705
723
  /**
706
724
  * @param {any} v
707
725
  * @param {(value: any) => any} [get]
@@ -722,57 +740,6 @@ export function track(v, get, set) {
722
740
  return tracked(v, get, set);
723
741
  }
724
742
 
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
743
  /**
777
744
  * @param {any} _
778
745
  * @param {ConstructorParameters<typeof URL>} params
@@ -865,6 +832,15 @@ export function ripple_object(obj) {
865
832
  return obj;
866
833
  }
867
834
 
835
+ /**
836
+ * @template K, V
837
+ * @param {Iterable<readonly [K, V]>} [iterable]
838
+ * @returns {Map<K, V>}
839
+ */
840
+ export function ripple_map(iterable) {
841
+ return new Map(iterable);
842
+ }
843
+
868
844
  /**
869
845
  * Returns the fallback value if the given value is undefined.
870
846
  * @template T
@@ -89,6 +89,34 @@ describe('basic client > errors', () => {
89
89
  );
90
90
  });
91
91
 
92
+ it('should throw error for calling children as a function', () => {
93
+ const code = `
94
+ export component Layout({ children }) {
95
+ {children()}
96
+ }
97
+ `;
98
+
99
+ expect(() => {
100
+ compile(code, 'test.ripple');
101
+ }).toThrow(
102
+ '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
103
+ );
104
+ });
105
+
106
+ it('should throw error for calling props.children as a function', () => {
107
+ const code = `
108
+ export component Layout(props) {
109
+ {props.children()}
110
+ }
111
+ `;
112
+
113
+ expect(() => {
114
+ compile(code, 'test.ripple');
115
+ }).toThrow(
116
+ '`children` cannot be called like a regular function. Use element syntax instead, e.g. `<children />` or `<props.children />`.',
117
+ );
118
+ });
119
+
92
120
  it('errors on mutating tracked value inside computed track() evaluation', () => {
93
121
  component Basic() {
94
122
  let &[count] = track(0);
@@ -1,5 +1,5 @@
1
1
  import type { PropsWithChildren, Tracked } from 'ripple';
2
- import { effect, flushSync, track, trackSplit, untrack } from 'ripple';
2
+ import { effect, flushSync, track, untrack } from 'ripple';
3
3
 
4
4
  describe('basic client > reactivity', () => {
5
5
  it('renders multiple reactive lexical blocks', () => {
@@ -384,162 +384,17 @@ describe('basic client > reactivity', () => {
384
384
  expect(state.finalValue).toBe(5);
385
385
  });
386
386
 
387
- describe('track/trackSplit APIs', () => {
388
- it('errors on invalid value as null for track with trackSplit', () => {
389
- component App() {
390
- let &[message] = track('');
391
-
392
- try {
393
- const [a, b, rest] = trackSplit(null, ['a', 'b']);
394
- } catch (e) {
395
- message = (e as Error).message;
396
- }
397
-
398
- <pre>{message}</pre>
399
- }
400
-
401
- render(App);
402
-
403
- const pre = container.querySelectorAll('pre')[0];
404
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
405
- });
406
-
407
- it('errors on invalid value as array for track with trackSplit', () => {
408
- component App() {
409
- let &[message] = track('');
410
-
411
- try {
412
- const [a, b, rest] = trackSplit([1, 2, 3], ['a', 'b']);
413
- } catch (e) {
414
- message = (e as Error).message;
415
- }
416
-
417
- <pre>{message}</pre>
418
- }
419
-
420
- render(App);
421
-
422
- const pre = container.querySelectorAll('pre')[0];
423
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
424
- });
425
-
426
- it('errors on invalid value as tracked for track with trackSplit', () => {
427
- component App() {
428
- const t = track({ a: 1, b: 2, c: 3 });
429
- let &[message] = track('');
430
-
431
- try {
432
- const [a, b, rest] = trackSplit(t, ['a', 'b']);
433
- } catch (e) {
434
- message = (e as Error).message;
435
- }
436
-
437
- <pre>{message}</pre>
438
- }
439
-
440
- render(App);
441
-
442
- const pre = container.querySelectorAll('pre')[0];
443
- expect(pre.textContent).toBe('Invalid value: expected a non-tracked object');
444
- });
445
-
446
- it('returns undefined for non-existent props in track with trackSplit', () => {
447
- component App() {
448
- const [a, b, rest] = trackSplit({ a: 1, c: 1 }, ['a', 'b']);
449
-
450
- <pre>{a.value}</pre>
451
- <pre>{String(b.value)}</pre>
452
- <pre>{rest.value.c}</pre>
453
- }
454
-
455
- render(App);
456
-
457
- const preA = container.querySelectorAll('pre')[0];
458
- const preB = container.querySelectorAll('pre')[1];
459
- const preC = container.querySelectorAll('pre')[2];
460
-
461
- expect(preA.textContent).toBe('1');
462
- expect(preB.textContent).toBe('undefined');
463
- expect(preC.textContent).toBe('1');
464
- });
465
-
466
- it('returns the same tracked object if plain track is called with a tracked object', () => {
467
- component App() {
468
- const t = track({ a: 1, b: 2, c: 3 });
469
- const doublet = track(t);
470
-
471
- <pre>{t === doublet}</pre>
472
- }
473
-
474
- render(App);
475
-
476
- const pre = container.querySelectorAll('pre')[0];
477
- expect(pre.textContent).toBe('true');
478
- });
479
-
480
- it('can retain reactivity for destructure rest via track trackSplit', () => {
481
- let logs: string[] = [];
482
-
483
- component App() {
484
- let &[count] = track(0);
485
- let &[name] = track('Click Me');
486
-
487
- function buttonRef(el: HTMLButtonElement) {
488
- logs.push('ref called');
489
- return () => {
490
- logs.push('cleanup ref');
491
- };
492
- }
493
-
494
- <Child
495
- class="my-button"
496
- onClick={() => name === 'Click Me' ? name = 'Clicked' : name = 'Click Me'}
497
- {count}
498
- {ref buttonRef}
499
- >
500
- {name}
501
- </Child>
502
-
503
- <button onClick={() => count++}>{'Increment Count'}</button>
504
- }
505
-
506
- component Child(props: PropsWithChildren<{
507
- count: Tracked<number>;
508
- class: string;
509
- onClick: () => void;
510
- }>) {
511
- const [children, count, rest] = trackSplit(props, ['children', 'count']);
512
-
513
- if (count.value < 2) {
514
- <button {...rest.value}>
515
- <@children />
516
- </button>
517
- }
518
- <pre>{count.value}</pre>
519
- }
520
-
521
- render(App);
522
- flushSync();
523
-
524
- const buttonClickMe = container.querySelectorAll('button')[0];
525
- const buttonIncrement = container.querySelectorAll('button')[1];
526
- const countPre = container.querySelector('pre');
527
-
528
- expect(buttonClickMe.textContent).toBe('Click Me');
529
- expect(countPre.textContent).toBe('0');
530
- expect(logs).toEqual(['ref called']);
531
-
532
- buttonClickMe.click();
533
- buttonIncrement.click();
534
- flushSync();
387
+ it('returns the same tracked object if plain track is called with a tracked object', () => {
388
+ component App() {
389
+ const t = track({ a: 1, b: 2, c: 3 });
390
+ const doublet = track(t);
535
391
 
536
- expect(buttonClickMe.textContent).toBe('Clicked');
537
- expect(countPre.textContent).toBe('1');
392
+ <pre>{t === doublet}</pre>
393
+ }
538
394
 
539
- buttonIncrement.click();
540
- flushSync();
395
+ render(App);
541
396
 
542
- expect(logs).toEqual(['ref called', 'cleanup ref']);
543
- });
397
+ const pre = container.querySelectorAll('pre')[0];
398
+ expect(pre.textContent).toBe('true');
544
399
  });
545
400
  });
@@ -341,18 +341,6 @@ component App() {
341
341
  });
342
342
 
343
343
  it('keeps lazy destructuring as plain destructuring in to_ts output', () => {
344
- const track_split_source = `
345
- import { trackSplit } from 'ripple';
346
- component App() {
347
- const source = { a: 1, b: 2, c: 3 };
348
- let &[a, b, rest] = trackSplit(source, ['a', 'b']);
349
- const sum = a + b + rest.c;
350
- }
351
- `;
352
- const track_split_result = compile_to_volar_mappings(track_split_source, 'test.ripple').code;
353
- expect(track_split_result).toContain('let [a, b, rest] = trackSplit(source, [\'a\', \'b\']);');
354
- expect(track_split_result).not.toContain('let lazy = trackSplit');
355
-
356
344
  const track_source = `
357
345
  import { track } from 'ripple';
358
346
  component App() {
@@ -369,6 +357,37 @@ component App() {
369
357
  expect(track_result).not.toContain('lazy0');
370
358
  });
371
359
 
360
+ it('uses tracked fast path for nested lazy params typed as Tracked', () => {
361
+ const source = `
362
+ import type { Tracked } from 'ripple';
363
+ function use_nested({ value: &[count, tracked] }: { value: Tracked<number> }) {
364
+ count++;
365
+ return tracked;
366
+ }
367
+ `;
368
+ const { js } = compile(source, 'tracked-nested-lazy.ripple', { mode: 'client' });
369
+
370
+ // Nested lazy array should still use tracked tuple fast path from outer annotation.
371
+ expect(js.code).toContain('_$_.update(');
372
+ expect(js.code).not.toContain('[0]');
373
+ expect(js.code).not.toContain('[1]');
374
+ });
375
+
376
+ it('uses tracked fast path for nested lazy params at tuple rest positions', () => {
377
+ const source = `
378
+ import type { Tracked } from 'ripple';
379
+ function use_tuple_rest({ value: [head, &[count, tracked]] }: { value: [number, ...Tracked<number>[]] }) {
380
+ count++;
381
+ return tracked;
382
+ }
383
+ `;
384
+ const { js } = compile(source, 'tracked-nested-lazy-tuple-rest.ripple', { mode: 'client' });
385
+
386
+ // Tuple rest element access should resolve to Tracked<number>, not Tracked<number>[].
387
+ expect(js.code).toContain('_$_.update(');
388
+ expect(js.code).not.toContain('[1]');
389
+ });
390
+
372
391
  it('preserves generic type args in interface extends for Volar mappings', () => {
373
392
  const source = `
374
393
  interface PolymorphicProps<T extends keyof HTMLElementTagNameMap> {
@@ -1,5 +1,5 @@
1
1
  import type { Tracked, Props } from 'ripple';
2
- import { effect, flushSync, track, trackSplit } from 'ripple';
2
+ import { effect, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('composite > props', () => {
5
5
  it('correctly handles default prop values', () => {
@@ -106,9 +106,8 @@ describe('composite > props', () => {
106
106
  });
107
107
 
108
108
  it('correctly retains prop accessors and reactivity when using rest props', () => {
109
- component Button(props: Props) {
110
- const [children, rest] = trackSplit(props, ['children']);
111
- <button {...rest.value}>
109
+ component Button(&{ children, ...rest }: Props) {
110
+ <button {...rest}>
112
111
  <@children />
113
112
  </button>
114
113
  <style>
@@ -121,10 +120,9 @@ describe('composite > props', () => {
121
120
  </style>
122
121
  }
123
122
 
124
- component Toggle(props: { pressed: Tracked<boolean> }) {
125
- const [pressed, rest] = trackSplit(props, ['pressed']);
123
+ component Toggle(&{ pressed, ...rest }: { pressed: Tracked<boolean> }) {
126
124
  const onClick = () => (pressed.value = !pressed.value);
127
- <Button {...rest.value} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
125
+ <Button {...rest} class={pressed.value ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
128
126
  <Button class={pressed.value ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
129
127
  }
130
128