ripple 0.2.105 → 0.2.106

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.
@@ -0,0 +1,147 @@
1
+ import { get, increment, safe_scope, tracked } from './internal/client/runtime.js';
2
+ import { get_current_url } from './url.js';
3
+
4
+ export const REPLACE = Symbol();
5
+
6
+
7
+ export class TrackedURLSearchParams extends URLSearchParams {
8
+ #block = safe_scope();
9
+ #version = tracked(0, this.#block);
10
+ #url = get_current_url();
11
+
12
+ #updating = false;
13
+
14
+ #update_url() {
15
+ if (!this.#url || this.#updating) return;
16
+ this.#updating = true;
17
+
18
+ const search = this.toString();
19
+ this.#url.search = search && `?${search}`;
20
+
21
+ this.#updating = false;
22
+ }
23
+
24
+ /**
25
+ * @param {URLSearchParams} params
26
+ * @internal
27
+ */
28
+ [REPLACE](params) {
29
+ if (this.#updating) return;
30
+ this.#updating = true;
31
+
32
+ for (const key of [...super.keys()]) {
33
+ super.delete(key);
34
+ }
35
+
36
+ for (const [key, value] of params) {
37
+ super.append(key, value);
38
+ }
39
+
40
+ increment(this.#version, this.#block);
41
+ this.#updating = false;
42
+ }
43
+
44
+ /**
45
+ * @param {string} name
46
+ * @param {string} value
47
+ * @returns {void}
48
+ */
49
+ append(name, value) {
50
+ super.append(name, value);
51
+ this.#update_url();
52
+ increment(this.#version, this.#block);
53
+ }
54
+
55
+ /**
56
+ * @param {string} name
57
+ * @param {string=} value
58
+ * @returns {void}
59
+ */
60
+ delete(name, value) {
61
+ var has_value = super.has(name, value);
62
+ super.delete(name, value);
63
+ if (has_value) {
64
+ this.#update_url();
65
+ increment(this.#version, this.#block);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * @param {string} name
71
+ * @returns {string|null}
72
+ */
73
+ get(name) {
74
+ get(this.#version);
75
+ return super.get(name);
76
+ }
77
+
78
+ /**
79
+ * @param {string} name
80
+ * @returns {string[]}
81
+ */
82
+ getAll(name) {
83
+ get(this.#version);
84
+ return super.getAll(name);
85
+ }
86
+
87
+ /**
88
+ * @param {string} name
89
+ * @param {string=} value
90
+ * @returns {boolean}
91
+ */
92
+ has(name, value) {
93
+ get(this.#version);
94
+ return super.has(name, value);
95
+ }
96
+
97
+ keys() {
98
+ get(this.#version);
99
+ return super.keys();
100
+ }
101
+
102
+ /**
103
+ * @param {string} name
104
+ * @param {string} value
105
+ * @returns {void}
106
+ */
107
+ set(name, value) {
108
+ var previous = super.getAll(name).join('');
109
+ super.set(name, value);
110
+ // can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3
111
+ // if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true
112
+ if (previous !== super.getAll(name).join('')) {
113
+ this.#update_url();
114
+ increment(this.#version, this.#block);
115
+ }
116
+ }
117
+
118
+ sort() {
119
+ super.sort();
120
+ this.#update_url();
121
+ increment(this.#version, this.#block);
122
+ }
123
+
124
+ toString() {
125
+ get(this.#version);
126
+ return super.toString();
127
+ }
128
+
129
+ values() {
130
+ get(this.#version);
131
+ return super.values();
132
+ }
133
+
134
+ entries() {
135
+ get(this.#version);
136
+ return super.entries();
137
+ }
138
+
139
+ [Symbol.iterator]() {
140
+ return this.entries();
141
+ }
142
+
143
+ get size() {
144
+ get(this.#version);
145
+ return super.size;
146
+ }
147
+ }
@@ -0,0 +1,164 @@
1
+ import { get, set, safe_scope, tracked } from './internal/client/runtime.js';
2
+ import { REPLACE, TrackedURLSearchParams } from './url-search-params.js';
3
+
4
+ /** @type {TrackedURL | null} */
5
+ let current_url = null;
6
+
7
+ export function get_current_url() {
8
+ return current_url;
9
+ }
10
+
11
+ export class TrackedURL extends URL {
12
+ #block = safe_scope();
13
+ #protocol = tracked(super.protocol, this.#block);
14
+ #username = tracked(super.username, this.#block);
15
+ #password = tracked(super.password, this.#block);
16
+ #hostname = tracked(super.hostname, this.#block);
17
+ #port = tracked(super.port, this.#block);
18
+ #pathname = tracked(super.pathname, this.#block);
19
+ #hash = tracked(super.hash, this.#block);
20
+ #search = tracked(super.search, this.#block);
21
+ #searchParams;
22
+
23
+ /**
24
+ * @param {string | URL} url
25
+ * @param {string | URL} [base]
26
+ */
27
+ constructor(url, base) {
28
+ url = new URL(url, base);
29
+ super(url);
30
+
31
+ current_url = this;
32
+ this.#searchParams = new TrackedURLSearchParams(url.searchParams);
33
+ current_url = null;
34
+ }
35
+
36
+ get hash() {
37
+ return get(this.#hash);
38
+ }
39
+
40
+ set hash(value) {
41
+ super.hash = value;
42
+ set(this.#hash, super.hash, this.#block);
43
+ }
44
+
45
+ get host() {
46
+ get(this.#hostname);
47
+ get(this.#port);
48
+ return super.host;
49
+ }
50
+
51
+ set host(value) {
52
+ super.host = value;
53
+ set(this.#hostname, super.hostname, this.#block);
54
+ set(this.#port, super.port, this.#block);
55
+ }
56
+
57
+ get hostname() {
58
+ return get(this.#hostname);
59
+ }
60
+
61
+ set hostname(value) {
62
+ super.hostname = value;
63
+ set(this.#hostname, super.hostname, this.#block);
64
+ }
65
+
66
+ get href() {
67
+ get(this.#protocol);
68
+ get(this.#username);
69
+ get(this.#password);
70
+ get(this.#hostname);
71
+ get(this.#port);
72
+ get(this.#pathname);
73
+ get(this.#hash);
74
+ get(this.#search);
75
+ return super.href;
76
+ }
77
+
78
+ set href(value) {
79
+ super.href = value;
80
+ set(this.#protocol, super.protocol, this.#block);
81
+ set(this.#username, super.username, this.#block);
82
+ set(this.#password, super.password, this.#block);
83
+ set(this.#hostname, super.hostname, this.#block);
84
+ set(this.#port, super.port, this.#block);
85
+ set(this.#pathname, super.pathname, this.#block);
86
+ set(this.#hash, super.hash, this.#block);
87
+ set(this.#search, super.search, this.#block);
88
+ this.#searchParams[REPLACE](super.searchParams);
89
+ }
90
+
91
+ get password() {
92
+ return get(this.#password);
93
+ }
94
+
95
+ set password(value) {
96
+ super.password = value;
97
+ set(this.#password, super.password, this.#block);
98
+ }
99
+
100
+ get pathname() {
101
+ return get(this.#pathname);
102
+ }
103
+
104
+ set pathname(value) {
105
+ super.pathname = value;
106
+ set(this.#pathname, super.pathname, this.#block);
107
+ }
108
+
109
+ get port() {
110
+ return get(this.#port);
111
+ }
112
+
113
+ set port(value) {
114
+ super.port = value;
115
+ set(this.#port, super.port, this.#block);
116
+ }
117
+
118
+ get protocol() {
119
+ return get(this.#protocol);
120
+ }
121
+
122
+ set protocol(value) {
123
+ super.protocol = value;
124
+ set(this.#protocol, super.protocol, this.#block);
125
+ }
126
+
127
+ get search() {
128
+ return get(this.#search);
129
+ }
130
+
131
+ set search(value) {
132
+ super.search = value;
133
+ set(this.#search, value, this.#block);
134
+ this.#searchParams[REPLACE](super.searchParams);
135
+ }
136
+
137
+ get username() {
138
+ return get(this.#username);
139
+ }
140
+
141
+ set username(value) {
142
+ super.username = value;
143
+ set(this.#username, super.username, this.#block);
144
+ }
145
+
146
+ get origin() {
147
+ get(this.#protocol);
148
+ get(this.#hostname);
149
+ get(this.#port);
150
+ return super.origin;
151
+ }
152
+
153
+ get searchParams() {
154
+ return this.#searchParams;
155
+ }
156
+
157
+ toString() {
158
+ return this.href;
159
+ }
160
+
161
+ toJSON() {
162
+ return this.href;
163
+ }
164
+ }
@@ -774,8 +774,39 @@ export function jsx_spread_attribute(argument) {
774
774
  };
775
775
  }
776
776
 
777
+
778
+ /**
779
+ * @returns {ESTree.SwitchStatement}
780
+ */
781
+ export function switch_builder(discriminant, cases) {
782
+ return {
783
+ type: 'SwitchStatement',
784
+ discriminant,
785
+ cases,
786
+ };
787
+ }
788
+
789
+ /**
790
+ * @returns {ESTree.SwitchCase}
791
+ */
792
+ export function switch_case(test = null, consequent = []) {
793
+ return {
794
+ type: 'SwitchCase',
795
+ test,
796
+ consequent,
797
+ };
798
+ }
799
+
777
800
  export const void0 = unary('void', literal(0));
778
801
 
802
+ /**
803
+ * @returns {ESTree.BreakStatement}
804
+ */
805
+ export const break_statement = {
806
+ type: 'BreakStatement',
807
+ label: null,
808
+ };
809
+
779
810
  export {
780
811
  await_builder as await,
781
812
  let_builder as let,
@@ -783,7 +814,9 @@ export {
783
814
  var_builder as var,
784
815
  true_instance as true,
785
816
  false_instance as false,
817
+ break_statement as break,
786
818
  for_builder as for,
819
+ switch_builder as switch,
787
820
  function_builder as function,
788
821
  return_builder as return,
789
822
  if_builder as if,
@@ -0,0 +1,12 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`compiler success tests > compiles tracked values in effect with assignment expression 1`] = `"state.count = _$_.get(count);"`;
4
+
5
+ exports[`compiler success tests > compiles tracked values in effect with update expressions 1`] = `
6
+ "_$_.with_scope(__block, () => untrack(() => {
7
+ state.preIncrement = _$_.update_pre(count, __block);
8
+ state.postIncrement = _$_.update(count, __block);
9
+ state.preDecrement = _$_.update_pre(count, __block, -1);
10
+ state.postDecrement = _$_.update(count, __block, -1);
11
+ }));"
12
+ `;
@@ -1843,5 +1843,50 @@ describe('basic client', () => {
1843
1843
 
1844
1844
  expect(error).toBe(undefined);
1845
1845
  });
1846
- });
1847
1846
 
1847
+ it('unwraps tracked values inside effect', () => {
1848
+ let state = {};
1849
+
1850
+ component Basic() {
1851
+ let count = track(0);
1852
+
1853
+ effect(() => {
1854
+ state.count = @count;
1855
+ })
1856
+ }
1857
+
1858
+ render(Basic);
1859
+ flushSync();
1860
+
1861
+ expect(state.count).toBe(0);
1862
+ });
1863
+
1864
+ it('does not unwrap values with update expressions inside effect', () => {
1865
+ let state = {};
1866
+
1867
+ component Basic() {
1868
+ let count = track(5);
1869
+
1870
+ effect(() => {
1871
+ untrack(() => {
1872
+ state.initialValue = @count;
1873
+ state.preIncrement = ++@count;
1874
+ state.postIncrement = @count++;
1875
+ state.preDecrement = --@count;
1876
+ state.postDecrement = @count--;
1877
+ state.finalValue = @count;
1878
+ });
1879
+ })
1880
+ }
1881
+
1882
+ render(Basic);
1883
+ flushSync();
1884
+
1885
+ expect(state.initialValue).toBe(5);
1886
+ expect(state.preIncrement).toBe(6);
1887
+ expect(state.postIncrement).toBe(6);
1888
+ expect(state.preDecrement).toBe(6);
1889
+ expect(state.postDecrement).toBe(6);
1890
+ expect(state.finalValue).toBe(5);
1891
+ });
1892
+ });
@@ -462,4 +462,37 @@ describe('compiler success tests', () => {
462
462
  expect(splitResult.textContent).toBe('Split result: Paragraph, Content');
463
463
  });
464
464
  });
465
+
466
+ it('compiles tracked values in effect with assignment expression', () => {
467
+ const source = `component App() {
468
+ let count = track(0);
469
+
470
+ effect(() => {
471
+ state.count = @count;
472
+ })
473
+ }`;
474
+ const result = compile(source, 'test.ripple');
475
+ // Extract just the effect callback body
476
+ const effectMatch = result.js.code.match(/effect\(\(\) => \{([^}]+)\}\)/s);
477
+ expect(effectMatch[1].trim()).toMatchSnapshot();
478
+ });
479
+
480
+ it('compiles tracked values in effect with update expressions', () => {
481
+ const source = `component App() {
482
+ let count = track(5);
483
+
484
+ effect(() => {
485
+ untrack(() => {
486
+ state.preIncrement = ++@count;
487
+ state.postIncrement = @count++;
488
+ state.preDecrement = --@count;
489
+ state.postDecrement = @count--;
490
+ });
491
+ })
492
+ }`;
493
+ const result = compile(source, 'test.ripple');
494
+ // Extract just the effect callback body
495
+ const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
496
+ expect(effectMatch[1].trim()).toMatchSnapshot();
497
+ });
465
498
  });
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mount, flushSync, MediaQuery, track } from 'ripple';
3
+
4
+ function setupMatchMedia() {
5
+ let listeners = new Set();
6
+
7
+ // A mock implementation of matchMedia
8
+ const mockMatchMedia = vi.fn().mockImplementation(query => {
9
+ return {
10
+ media: query,
11
+ matches: false, // default value
12
+ addEventListener: (type, cb) => {
13
+ if (type === 'change') listeners.add(cb);
14
+ },
15
+ removeEventListener: (type, cb) => {
16
+ if (type === 'change') listeners.delete(cb);
17
+ },
18
+ /** @param {function(MediaQueryListEvent): void} cb */
19
+ addListener: (cb) => listeners.add(cb),
20
+ /** @param {function(MediaQueryListEvent): void} cb */
21
+ removeListener: (cb) => listeners.delete(cb),
22
+ dispatch: (event) => {
23
+ listeners.forEach((cb) => cb(event));
24
+ },
25
+ listenersCount: () => listeners.size,
26
+ };
27
+ });
28
+
29
+ Object.defineProperty(window, 'matchMedia', {
30
+ writable: true,
31
+ value: mockMatchMedia
32
+ });
33
+
34
+ return { mockMatchMedia, listeners };
35
+ }
36
+
37
+ describe('MediaQuery', () => {
38
+ let container;
39
+ let mm;
40
+
41
+ function render(component) {
42
+ mount(component, {
43
+ target: container
44
+ });
45
+ }
46
+
47
+ beforeEach(() => {
48
+ mm = setupMatchMedia();
49
+ container = document.createElement('div');
50
+ document.body.appendChild(container);
51
+ });
52
+
53
+ afterEach(() => {
54
+ document.body.removeChild(container);
55
+ container = null;
56
+ });
57
+
58
+ it('should be reactive if matchMedia changes', () => {
59
+ const media = '(min-width: 600px)';
60
+
61
+ component App() {
62
+ const medium = new MediaQuery(media);
63
+
64
+ <div>
65
+ <p>{@medium}</p>
66
+ </div>
67
+ }
68
+
69
+ render(App);
70
+ flushSync();
71
+
72
+ const event = new Event('change');
73
+ Object.assign(event, { matches: true, media: media });
74
+
75
+ const p = container.querySelector('p');
76
+ expect(p.textContent).toBe('false');
77
+
78
+
79
+ mm.mockMatchMedia.mock.results[0].value.matches = true;
80
+ mm.mockMatchMedia.mock.results[0].value.dispatch(event);
81
+ flushSync();
82
+
83
+ expect(p.textContent).toBe('true');
84
+
85
+ Object.assign(event, { matches: false, media: media });
86
+
87
+ mm.mockMatchMedia.mock.results[0].value.matches = false;
88
+ mm.mockMatchMedia.mock.results[0].value.dispatch(event);
89
+ flushSync();
90
+
91
+ expect(p.textContent).toBe('false');
92
+ });
93
+
94
+
95
+ it('should have cleared event listeners after unmount', async () => {
96
+ const media = '(min-width: 600px)';
97
+
98
+ component App() {
99
+ let show = track(true);
100
+
101
+ if (@show) {
102
+ <Child />
103
+ }
104
+
105
+ <button onClick={() => @show = !@show}>{'Toggle Child'}</button>
106
+ }
107
+
108
+ component Child() {
109
+ const medium = new MediaQuery(media);
110
+
111
+ <div>
112
+ <p>{@medium}</p>
113
+ </div>
114
+ }
115
+
116
+ render(App);
117
+ flushSync();
118
+
119
+ expect(mm.mockMatchMedia.mock.results[0].value.listenersCount()).toBe(1);
120
+
121
+ const button = container.querySelector('button');
122
+ button.click();
123
+ flushSync();
124
+
125
+ // wait for microtask queue to flush
126
+ await new Promise(resolve => setTimeout(resolve, 0));
127
+
128
+ expect(mm.mockMatchMedia.mock.results[0].value.listenersCount()).toBe(0);
129
+ });
130
+ });