ripple 0.3.64 → 0.3.66

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 (63) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +3 -3
  3. package/src/jsx-runtime.d.ts +17 -3
  4. package/src/runtime/index-client.js +47 -8
  5. package/src/runtime/index-server.js +0 -2
  6. package/src/runtime/internal/client/index.js +5 -0
  7. package/src/runtime/internal/client/runtime.js +111 -11
  8. package/src/runtime/internal/client/types.d.ts +5 -0
  9. package/src/runtime/internal/server/blocks.js +1 -0
  10. package/src/runtime/internal/server/index.js +175 -20
  11. package/src/utils/errors.js +13 -0
  12. package/tests/client/async-suspend.test.tsrx +2 -2
  13. package/tests/client/basic/basic.get-set.test.tsrx +26 -26
  14. package/tests/client/compiler/__snapshots__/compiler.assignments.test.rsrx.snap +1 -1
  15. package/tests/client/compiler/__snapshots__/compiler.assignments.test.tsrx.snap +1 -1
  16. package/tests/client/compiler/compiler.assignments.test.tsrx +80 -0
  17. package/tests/client/compiler/compiler.tracked-access.test.tsrx +52 -8
  18. package/tests/client/compiler/compiler.typescript.test.tsrx +23 -0
  19. package/tests/client/lazy-array.test.tsrx +34 -0
  20. package/tests/client/lazy-destructuring.test.tsrx +79 -8
  21. package/tests/client/tracked-index-access.test.tsrx +113 -0
  22. package/tests/client/tsx.test.tsrx +66 -21
  23. package/tests/hydration/compiled/client/basic.js +2 -2
  24. package/tests/hydration/compiled/client/events.js +9 -9
  25. package/tests/hydration/compiled/client/for.js +50 -54
  26. package/tests/hydration/compiled/client/head.js +9 -9
  27. package/tests/hydration/compiled/client/hmr.js +1 -1
  28. package/tests/hydration/compiled/client/html.js +2 -2
  29. package/tests/hydration/compiled/client/if-children.js +14 -14
  30. package/tests/hydration/compiled/client/if.js +10 -10
  31. package/tests/hydration/compiled/client/mixed-control-flow.js +7 -7
  32. package/tests/hydration/compiled/client/portal.js +2 -2
  33. package/tests/hydration/compiled/client/reactivity.js +7 -7
  34. package/tests/hydration/compiled/client/return.js +37 -37
  35. package/tests/hydration/compiled/client/switch.js +8 -8
  36. package/tests/hydration/compiled/client/track-async-serialization.js +12 -12
  37. package/tests/hydration/compiled/client/try.js +116 -33
  38. package/tests/hydration/compiled/server/basic.js +2 -2
  39. package/tests/hydration/compiled/server/events.js +8 -8
  40. package/tests/hydration/compiled/server/for.js +21 -21
  41. package/tests/hydration/compiled/server/head.js +10 -10
  42. package/tests/hydration/compiled/server/hmr.js +1 -1
  43. package/tests/hydration/compiled/server/html.js +1 -1
  44. package/tests/hydration/compiled/server/if-children.js +9 -9
  45. package/tests/hydration/compiled/server/if.js +6 -6
  46. package/tests/hydration/compiled/server/mixed-control-flow.js +4 -4
  47. package/tests/hydration/compiled/server/portal.js +1 -1
  48. package/tests/hydration/compiled/server/reactivity.js +7 -7
  49. package/tests/hydration/compiled/server/return.js +14 -14
  50. package/tests/hydration/compiled/server/switch.js +4 -4
  51. package/tests/hydration/compiled/server/track-async-serialization.js +12 -12
  52. package/tests/hydration/compiled/server/try.js +116 -4
  53. package/tests/hydration/components/basic.tsrx +3 -1
  54. package/tests/hydration/components/try.tsrx +26 -0
  55. package/tests/hydration/try.test.js +100 -1
  56. package/tests/server/await.test.tsrx +1 -1
  57. package/tests/server/basic.test.tsrx +3 -1
  58. package/tests/server/compiler.test.tsrx +109 -0
  59. package/tests/server/lazy-destructuring.test.tsrx +62 -0
  60. package/tests/server/tracked-index-access.test.tsrx +76 -0
  61. package/tests/setup-hydration.js +31 -0
  62. package/types/index.d.ts +11 -9
  63. package/types/server.d.ts +2 -1
@@ -28,7 +28,7 @@ import {
28
28
  } from '../client/constants.js';
29
29
  import { DEV } from 'esm-env';
30
30
  import { is_ripple_object } from '../client/utils.js';
31
- import { array_slice, is_array } from '@tsrx/core/runtime/language-helpers';
31
+ import { iterable_array_from, array_slice, is_array } from '@tsrx/core/runtime/language-helpers';
32
32
  import {
33
33
  escape,
34
34
  escape_script,
@@ -44,6 +44,10 @@ import {
44
44
  is_tag_valid_with_ancestor,
45
45
  } from '../../../html-tree-validation.js';
46
46
  import { get_async_track_result } from '../../../utils/async.js';
47
+ import {
48
+ throw_tracked_index_reference_error,
49
+ throw_tracked_index_value_error,
50
+ } from '../../../utils/errors.js';
47
51
  import { get_track_async_script_id } from '../../../utils/track-async-serialization.js';
48
52
  import * as devalue from 'devalue';
49
53
  import {
@@ -696,17 +700,26 @@ export async function render(component, passed_in_options = {}) {
696
700
  }
697
701
  },
698
702
  (error) => {
699
- // TODO - allow a global error template in ripple.config.ts
700
703
  // We're not going to send the error in the stream stream.error()
701
704
  // as we should send sent the error template
702
705
 
703
706
  // store the error to be returned
704
707
  top_level_error = error;
705
- console.error(error);
708
+ const output = /** @type {Block | null} */ (root_block)?.o;
709
+ if (output?.isSyncRun()) {
710
+ output._decrementPending();
711
+ output._finishSyncRun();
712
+ }
713
+ if (options.rootBoundary?.catch) {
714
+ options.rootBoundary.catch({ error, reset: noop });
715
+ } else {
716
+ console.error(error);
717
+ }
706
718
  },
707
719
  () => {
708
- // TODO - allow a global pending in ripple.config.ts
709
- // pending would be implemented as part of the streaming rendering support
720
+ if (options.rootBoundary?.pending) {
721
+ options.rootBoundary.pending({});
722
+ }
710
723
  },
711
724
  );
712
725
 
@@ -863,12 +876,49 @@ export function get(tracked) {
863
876
  return tracked;
864
877
  }
865
878
 
866
- if ((tracked.f & DERIVED) !== 0) {
867
- update_derived(/** @type {Derived} **/ (tracked));
868
- if (tracking) {
869
- register_dependency(tracked);
879
+ return (tracked.f & DERIVED) !== 0
880
+ ? get_derived(/** @type {Derived} */ (tracked))
881
+ : get_tracked(/** @type {Tracked} */ (tracked));
882
+ }
883
+
884
+ /**
885
+ * @param {Tracked} tracked
886
+ * @returns {any}
887
+ */
888
+ export function get_tracked(tracked) {
889
+ if (tracking) {
890
+ register_dependency(tracked);
891
+ }
892
+
893
+ if (tracked.v === SUSPENSE_PENDING || tracked.v === SUSPENSE_REJECTED) {
894
+ var is_try_block = false;
895
+ if (
896
+ !inside_async_track &&
897
+ (!active_block ||
898
+ active_block.f & COMPONENT_BLOCK ||
899
+ (is_try_block = (active_block.f & TRY_BLOCK) !== 0))
900
+ ) {
901
+ throw new Error(
902
+ `Reads on pending tracked or derived values directly inside ${is_try_block ? 'try' : 'component'} body are prohibited. Use trackPending() test for safe access or create another derived instead.`,
903
+ );
870
904
  }
871
- } else if (tracking) {
905
+
906
+ // this will be caught by the run_block and the block will be re-run
907
+ // once the async tracked dependency's promise resolves
908
+ throw ASYNC_DERIVED_READ_THROWN;
909
+ }
910
+
911
+ var g = tracked.a.get;
912
+ return g ? g(tracked.v) : tracked.v;
913
+ }
914
+
915
+ /**
916
+ * @param {Derived} tracked
917
+ * @returns {any}
918
+ */
919
+ export function get_derived(tracked) {
920
+ update_derived(tracked);
921
+ if (tracking) {
872
922
  register_dependency(tracked);
873
923
  }
874
924
 
@@ -894,6 +944,60 @@ export function get(tracked) {
894
944
  return g ? g(tracked.v) : tracked.v;
895
945
  }
896
946
 
947
+ /**
948
+ * @param {any} lazy
949
+ * @param {number} [index]
950
+ * @returns {any}
951
+ */
952
+ export function lazy_array_get(lazy, index = 0) {
953
+ if (is_array(lazy)) {
954
+ return lazy[index];
955
+ }
956
+ var flags = lazy.f;
957
+ if (flags === TRACKED) {
958
+ return index === 0
959
+ ? get_tracked(/** @type {Tracked} */ (lazy))
960
+ : index === 1
961
+ ? lazy
962
+ : undefined;
963
+ }
964
+ if (flags === DERIVED) {
965
+ return index === 0
966
+ ? get_derived(/** @type {Derived} */ (lazy))
967
+ : index === 1
968
+ ? lazy
969
+ : undefined;
970
+ }
971
+ return iterable_array_from(lazy, index)[0];
972
+ }
973
+
974
+ /**
975
+ * @param {any} lazy
976
+ * @param {number} [index]
977
+ * @returns {any[]}
978
+ */
979
+ export function lazy_array_rest(lazy, index = 0) {
980
+ if (is_array(lazy)) {
981
+ return lazy.slice(index);
982
+ }
983
+ var flags = lazy.f;
984
+ if (flags === TRACKED) {
985
+ return index === 0
986
+ ? [get_tracked(/** @type {Tracked} */ (lazy)), lazy]
987
+ : index === 1
988
+ ? [lazy]
989
+ : [];
990
+ }
991
+ if (flags === DERIVED) {
992
+ return index === 0
993
+ ? [get_derived(/** @type {Derived} */ (lazy)), lazy]
994
+ : index === 1
995
+ ? [lazy]
996
+ : [];
997
+ }
998
+ return iterable_array_from(lazy, index);
999
+ }
1000
+
897
1001
  /**
898
1002
  * @param {Derived | Tracked} tracked
899
1003
  * @param {any} value
@@ -908,6 +1012,57 @@ export function set(tracked, value) {
908
1012
  }
909
1013
  }
910
1014
 
1015
+ /**
1016
+ * @param {any} lazy
1017
+ * @param {any} value
1018
+ * @param {number} [index]
1019
+ * @returns {void}
1020
+ */
1021
+ export function lazy_array_set(lazy, value, index = 0) {
1022
+ if (is_array(lazy)) {
1023
+ lazy[index] = value;
1024
+ return;
1025
+ }
1026
+ var flags = lazy.f;
1027
+ if (flags === TRACKED || flags === DERIVED) {
1028
+ if (index === 0) {
1029
+ set(/** @type {Derived | Tracked} */ (lazy), value);
1030
+ return;
1031
+ }
1032
+ if (index === 1) {
1033
+ throw_tracked_index_reference_error();
1034
+ }
1035
+ return;
1036
+ }
1037
+ lazy[index] = value;
1038
+ }
1039
+
1040
+ /**
1041
+ * @param {any} lazy
1042
+ * @param {number} [index]
1043
+ * @param {number} [d]
1044
+ * @returns {number}
1045
+ */
1046
+ export function lazy_array_update(lazy, index = 0, d = 1) {
1047
+ var value = lazy_array_get(lazy, index);
1048
+ var result = d === 1 ? value++ : value--;
1049
+ lazy_array_set(lazy, value, index);
1050
+ return result;
1051
+ }
1052
+
1053
+ /**
1054
+ * @param {any} lazy
1055
+ * @param {number} [index]
1056
+ * @param {number} [d]
1057
+ * @returns {number}
1058
+ */
1059
+ export function lazy_array_update_pre(lazy, index = 0, d = 1) {
1060
+ var value = lazy_array_get(lazy, index);
1061
+ var new_value = d === 1 ? ++value : --value;
1062
+ lazy_array_set(lazy, new_value, index);
1063
+ return new_value;
1064
+ }
1065
+
911
1066
  /**
912
1067
  * @param {Tracked} tracked
913
1068
  * @param {number} [d]
@@ -1082,19 +1237,19 @@ class TrackedValue {
1082
1237
  }
1083
1238
  /** @returns {any} */
1084
1239
  get [0]() {
1085
- return get(/** @type {Tracked} */ (this));
1240
+ return throw_tracked_index_value_error();
1086
1241
  }
1087
1242
  /** @param {any} v */
1088
1243
  set [0](v) {
1089
- set(/** @type {Tracked} */ (this), v);
1244
+ throw_tracked_index_value_error();
1090
1245
  }
1091
1246
  /** @returns {Tracked} */
1092
1247
  get [1]() {
1093
- return /** @type {Tracked} */ (this);
1248
+ return throw_tracked_index_reference_error();
1094
1249
  }
1095
1250
  /** @returns {any} */
1096
1251
  get value() {
1097
- return get(/** @type {Tracked} */ (this));
1252
+ return get_tracked(/** @type {Tracked} */ (this));
1098
1253
  }
1099
1254
  /** @param {any} v */
1100
1255
  set value(v) {
@@ -1106,7 +1261,7 @@ class TrackedValue {
1106
1261
  }
1107
1262
  /** @returns {Iterator<any | Tracked>} */
1108
1263
  *[Symbol.iterator]() {
1109
- yield get(/** @type {Tracked} */ (this));
1264
+ yield get_tracked(/** @type {Tracked} */ (this));
1110
1265
  yield this;
1111
1266
  }
1112
1267
  }
@@ -1140,19 +1295,19 @@ class DerivedValue {
1140
1295
  }
1141
1296
  /** @returns {any} */
1142
1297
  get [0]() {
1143
- return get(/** @type {Derived} */ (this));
1298
+ return throw_tracked_index_value_error();
1144
1299
  }
1145
1300
  /** @param {any} v */
1146
1301
  set [0](v) {
1147
- set(/** @type {Derived} */ (this), v);
1302
+ throw_tracked_index_value_error();
1148
1303
  }
1149
1304
  /** @returns {Derived} */
1150
1305
  get [1]() {
1151
- return /** @type {Derived} */ (this);
1306
+ return throw_tracked_index_reference_error();
1152
1307
  }
1153
1308
  /** @returns {any} */
1154
1309
  get value() {
1155
- return get(/** @type {Derived} */ (this));
1310
+ return get_derived(/** @type {Derived} */ (this));
1156
1311
  }
1157
1312
  /** @param {any} v */
1158
1313
  set value(v) {
@@ -1164,7 +1319,7 @@ class DerivedValue {
1164
1319
  }
1165
1320
  /** @returns {Iterator<any | Derived>} */
1166
1321
  *[Symbol.iterator]() {
1167
- yield get(/** @type {Derived} */ (this));
1322
+ yield get_derived(/** @type {Derived} */ (this));
1168
1323
  yield this;
1169
1324
  }
1170
1325
  }
@@ -0,0 +1,13 @@
1
+ /** @returns {never} */
2
+ export function throw_tracked_index_value_error() {
3
+ throw new Error(
4
+ 'Do not access tracked values with [0]. Use .value or &[] lazy destructuring instead. Numeric tracked access leads to degraded performance.',
5
+ );
6
+ }
7
+
8
+ /** @returns {never} */
9
+ export function throw_tracked_index_reference_error() {
10
+ throw new Error(
11
+ 'Do not access tracked values with [1]. Use the tracked value directly instead. Numeric tracked access leads to degraded performance.',
12
+ );
13
+ }
@@ -659,7 +659,7 @@ describe('async suspense', () => {
659
659
  },
660
660
  );
661
661
 
662
- it('throws when trackAsync is used without a try/pending boundary', () => {
662
+ it('uses the root pending boundary when trackAsync has no local try/pending boundary', () => {
663
663
  component App() {
664
664
  let &[value] = trackAsync(() => Promise.resolve('test'));
665
665
  <div>{value}</div>
@@ -667,6 +667,6 @@ describe('async suspense', () => {
667
667
 
668
668
  expect(() => {
669
669
  render(App);
670
- }).toThrow('Missing parent `try { ... } pending { ... }` statement');
670
+ }).not.toThrow();
671
671
  });
672
672
  });
@@ -1,11 +1,11 @@
1
- import { effect, flushSync, get, set, track, untrack } from 'ripple';
1
+ import { effect, flushSync, track, untrack } from 'ripple';
2
2
 
3
- describe('basic client > get/set functions', () => {
3
+ describe('basic client > tracked value access', () => {
4
4
  it('gets tracked value', () => {
5
5
  component Test() {
6
6
  let count = track(0);
7
7
 
8
- <div>{get(count)}</div>
8
+ <div>{count.value}</div>
9
9
  }
10
10
 
11
11
  render(Test);
@@ -18,7 +18,7 @@ describe('basic client > get/set functions', () => {
18
18
  component Test() {
19
19
  let &[count] = track(0);
20
20
 
21
- <p>{get(count)}</p>
21
+ <p>{count}</p>
22
22
  <button onClick={() => count++}>{'increment'}</button>
23
23
  }
24
24
 
@@ -38,7 +38,7 @@ describe('basic client > get/set functions', () => {
38
38
  component Test() {
39
39
  let &[count] = track(0);
40
40
 
41
- <p>{get(count)}</p>
41
+ <p>{count}</p>
42
42
  <button
43
43
  onClick={() => {
44
44
  count++;
@@ -66,8 +66,8 @@ describe('basic client > get/set functions', () => {
66
66
  component Test() {
67
67
  let count = track(0);
68
68
 
69
- <p>{get(count)}</p>
70
- <button onClick={() => set(count, 10)}>{'set to 10'}</button>
69
+ <p>{count.value}</p>
70
+ <button onClick={() => (count.value = 10)}>{'set to 10'}</button>
71
71
  }
72
72
 
73
73
  render(Test);
@@ -86,12 +86,12 @@ describe('basic client > get/set functions', () => {
86
86
  component Test() {
87
87
  let count = track(0);
88
88
 
89
- <p>{get(count)}</p>
89
+ <p>{count.value}</p>
90
90
  <button
91
91
  onClick={() => {
92
- set(count, 5);
93
- set(count, 15);
94
- set(count, 25);
92
+ count.value = 5;
93
+ count.value = 15;
94
+ count.value = 25;
95
95
  }}
96
96
  >
97
97
  {'set multiple times'}
@@ -114,8 +114,8 @@ describe('basic client > get/set functions', () => {
114
114
  component Test() {
115
115
  let count = track(0);
116
116
 
117
- <p>{get(count)}</p>
118
- <button onClick={() => set(count, get(count) + 10)}>{'add 10'}</button>
117
+ <p>{count.value}</p>
118
+ <button onClick={() => (count.value = count.value + 10)}>{'add 10'}</button>
119
119
  }
120
120
 
121
121
  render(Test);
@@ -140,12 +140,12 @@ describe('basic client > get/set functions', () => {
140
140
  component Test() {
141
141
  let count = track(0);
142
142
 
143
- <p>{get(count)}</p>
143
+ <p>{count.value}</p>
144
144
  <button
145
145
  onClick={() => {
146
- set(count, get(count) + 5);
147
- set(count, get(count) + 15);
148
- set(count, get(count) + 25);
146
+ count.value = count.value + 5;
147
+ count.value = count.value + 15;
148
+ count.value = count.value + 25;
149
149
  }}
150
150
  >
151
151
  {'add multiple times'}
@@ -177,7 +177,7 @@ describe('basic client > get/set functions', () => {
177
177
 
178
178
  component Test() {
179
179
  let count = store();
180
- <p>{get(count)}</p>
180
+ <p>{count.value}</p>
181
181
  }
182
182
 
183
183
  render(Test);
@@ -194,8 +194,8 @@ describe('basic client > get/set functions', () => {
194
194
  component Test() {
195
195
  let count = store();
196
196
 
197
- <p>{get(count)}</p>
198
- <button onClick={() => set(count, 50)}>{'set to 50'}</button>
197
+ <p>{count.value}</p>
198
+ <button onClick={() => (count.value = 50)}>{'set to 50'}</button>
199
199
  }
200
200
 
201
201
  render(Test);
@@ -216,11 +216,11 @@ describe('basic client > get/set functions', () => {
216
216
  let double = track(0);
217
217
 
218
218
  effect(() => {
219
- set(double, get(count) * 2);
219
+ double.value = count.value * 2;
220
220
  });
221
221
 
222
- <p>{get(double)}</p>
223
- <button onClick={() => set(count, get(count) + 1)}>{'increment'}</button>
222
+ <p>{double.value}</p>
223
+ <button onClick={() => (count.value = count.value + 1)}>{'increment'}</button>
224
224
  }
225
225
 
226
226
  render(Test);
@@ -247,12 +247,12 @@ describe('basic client > get/set functions', () => {
247
247
 
248
248
  effect(() => {
249
249
  untrack(() => {
250
- set(double, get(count) * 2);
250
+ double.value = count.value * 2;
251
251
  });
252
252
  });
253
253
 
254
- <p>{get(double)}</p>
255
- <button onClick={() => set(count, get(count) + 1)}>{'increment'}</button>
254
+ <p>{double.value}</p>
255
+ <button onClick={() => (count.value = count.value + 1)}>{'increment'}</button>
256
256
  }
257
257
 
258
258
  render(Test);
@@ -1,6 +1,6 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(lazy);"`;
3
+ exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = lazy.value;"`;
4
4
 
5
5
  exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
6
6
  "_$_.untrack(() => {
@@ -1,6 +1,6 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(lazy);"`;
3
+ exports[`compiler > assignments > compiles tracked values in effect with assignment expression 1`] = `"state.count = lazy.value;"`;
4
4
 
5
5
  exports[`compiler > assignments > compiles tracked values in effect with update expressions 1`] = `
6
6
  "_$_.untrack(() => {
@@ -139,4 +139,84 @@ component App() {
139
139
  const effect_match = result.code.match(EFFECT_BODY_REGEX);
140
140
  expect(effect_match?.[1].trim()).toMatchSnapshot();
141
141
  });
142
+
143
+ it('compiles unknown lazy array destructuring through lazy array helpers', () => {
144
+ const source = `import { track } from 'ripple';
145
+ component Child({ tr: &[count, tr] }) {
146
+ count++;
147
+ tr[0]++;
148
+ <div>{count}</div>
149
+ }
150
+ component App() {
151
+ let tracked = track(0);
152
+ <Child tr={tracked} />
153
+ }`;
154
+ const { code } = compile(source, 'test.tsrx', { mode: 'client' });
155
+
156
+ expect(code).toContain('_$_.lazy_array_get(lazy, 0)');
157
+ expect(code).toContain('_$_.lazy_array_update(lazy, 0)');
158
+ expect(code).toContain('_$_.lazy_array_get(lazy, 1)');
159
+ expect(code).toContain('_$_.lazy_array_update(_$_.lazy_array_get(lazy, 1), 0)');
160
+ expect(code).not.toContain('lazy[0]');
161
+ expect(code).not.toContain('lazy[1][0]');
162
+ });
163
+
164
+ it('compiles writes to unknown lazy array index 1 through lazy array helpers', () => {
165
+ const source = `component Child({ pair: &[first, second] }) {
166
+ second = 10;
167
+ <div>{first}</div>
168
+ }
169
+ component App() {
170
+ <Child pair={[0, 1]} />
171
+ }`;
172
+ const { code } = compile(source, 'test.tsrx', { mode: 'client' });
173
+
174
+ expect(code).toContain('_$_.lazy_array_set(lazy, 10, 1)');
175
+ expect(code).not.toContain('lazy[1] =');
176
+ });
177
+
178
+ it('does not double-wrap member access on lazy array value bindings', () => {
179
+ const source = `component Child({ pair: &[first] }) {
180
+ let value = first[0];
181
+ <div>{value}</div>
182
+ }
183
+ component App() {
184
+ <Child pair={[{ 0: 'x' }]} />
185
+ }`;
186
+ const { code } = compile(source, 'test.tsrx', { mode: 'client' });
187
+
188
+ expect(code).toContain('let value = _$_.lazy_array_get(lazy, 0)[0];');
189
+ expect(code).not.toContain('_$_.lazy_array_get(_$_.lazy_array_get(lazy, 0), 0)');
190
+ });
191
+
192
+ it('throws on indexed access through known tracked lazy destructures', () => {
193
+ const source = `import { track } from 'ripple';
194
+ component App() {
195
+ let &[value, tracked_ref] = track({ 0: 'x' });
196
+ let nested = value[0];
197
+ tracked_ref[0] = { 0: 'y' };
198
+ let next = value[0];
199
+ <div>{nested}{next}</div>
200
+ }`;
201
+
202
+ expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
203
+ /Use \.value or &\[\] lazy destructuring/,
204
+ );
205
+ });
206
+
207
+ it('throws on known tracked indexed access', () => {
208
+ const source = `import { track } from 'ripple';
209
+ component App() {
210
+ let tracked = track(0);
211
+ tracked[0]++;
212
+ tracked[0] = tracked[0] + 1;
213
+ let value = tracked[0];
214
+ let ref = tracked[1];
215
+ <div>{value}</div>
216
+ }`;
217
+
218
+ expect(() => compile(source, 'test.tsrx', { mode: 'client' })).toThrow(
219
+ /Use \.value or &\[\] lazy destructuring/,
220
+ );
221
+ });
142
222
  });
@@ -1,5 +1,8 @@
1
1
  import { compile } from '@tsrx/ripple';
2
2
 
3
+ const value_message = /Use \.value or &\[\] lazy destructuring/;
4
+ const reference_message = /Use the tracked value directly instead/;
5
+
3
6
  describe('Compiler: Tracked Object Direct Access Checks', () => {
4
7
  it('should error on direct access to __v of a tracked object', () => {
5
8
  const code = `
@@ -77,12 +80,12 @@ import { track } from 'ripple';
77
80
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
78
81
  });
79
82
 
80
- it('should compile successfully with correct get() function access', () => {
83
+ it('should compile successfully with correct value access', () => {
81
84
  const code = `
82
- import { get, track } from 'ripple';
85
+ import { track } from 'ripple';
83
86
  export default component App() {
84
87
  let count = track(0);
85
- console.log(get(count));
88
+ console.log(count.value);
86
89
  }
87
90
  `;
88
91
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
@@ -122,7 +125,7 @@ import { get, track } from 'ripple';
122
125
  },
123
126
  );
124
127
 
125
- it('should allow indexed [0] access on a tracked object', () => {
128
+ it('should error on indexed [0] access on a tracked object', () => {
126
129
  const code = `
127
130
  import { track } from 'ripple';
128
131
  export default component App() {
@@ -130,10 +133,10 @@ import { track } from 'ripple';
130
133
  console.log(count[0]);
131
134
  }
132
135
  `;
133
- expect(() => compile(code, 'test.tsrx')).not.toThrow();
136
+ expect(() => compile(code, 'test.tsrx')).toThrow(value_message);
134
137
  });
135
138
 
136
- it('should allow indexed [1] access on a tracked object', () => {
139
+ it('should error on indexed [1] access on a tracked object', () => {
137
140
  const code = `
138
141
  import { track } from 'ripple';
139
142
  export default component App() {
@@ -141,16 +144,57 @@ import { track } from 'ripple';
141
144
  let raw = count[1];
142
145
  }
143
146
  `;
144
- expect(() => compile(code, 'test.tsrx')).not.toThrow();
147
+ expect(() => compile(code, 'test.tsrx')).toThrow(reference_message);
145
148
  });
146
149
 
147
- it('should allow [0] write on a tracked object', () => {
150
+ it('should error on indexed [0] write on a tracked object', () => {
148
151
  const code = `
149
152
  import { track } from 'ripple';
150
153
  export default component App() {
151
154
  let count = track(0);
152
155
  count[0] = 5;
153
156
  }
157
+ `;
158
+ expect(() => compile(code, 'test.tsrx')).toThrow(value_message);
159
+ });
160
+
161
+ it('collects tracked numeric index errors in loose mode', () => {
162
+ const code = `
163
+ import { track } from 'ripple';
164
+ export default component App() {
165
+ let count = track(0);
166
+ console.log(count[0]);
167
+ let raw = count[1];
168
+ }
169
+ `;
170
+ const result = compile(code, 'test.tsrx', { loose: true });
171
+
172
+ expect(result.errors.map((error) => error.message)).toEqual(
173
+ expect.arrayContaining([
174
+ expect.stringMatching(value_message),
175
+ expect.stringMatching(reference_message),
176
+ ]),
177
+ );
178
+ });
179
+
180
+ it('should error on indexed access through a known tracked lazy ref binding', () => {
181
+ const code = `
182
+ import { track } from 'ripple';
183
+ export default component App() {
184
+ let &[value, tracked_ref] = track(0);
185
+ tracked_ref[0]++;
186
+ }
187
+ `;
188
+ expect(() => compile(code, 'test.tsrx')).toThrow(value_message);
189
+ });
190
+
191
+ it('should allow lazy destructuring a tracked value', () => {
192
+ const code = `
193
+ import { track } from 'ripple';
194
+ export default component App() {
195
+ let &[value, tracked_ref] = track(0);
196
+ console.log(value, tracked_ref.value);
197
+ }
154
198
  `;
155
199
  expect(() => compile(code, 'test.tsrx')).not.toThrow();
156
200
  });