ripple 0.3.56 → 0.3.58

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.58
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1130](https://github.com/Ripple-TS/ripple/pull/1130)
8
+ [`0a5f39b`](https://github.com/Ripple-TS/ripple/commit/0a5f39b6e13807dfd3dc1228f40d7bb02b933373)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fix client cleanup for
10
+ HMR-wrapped roots that do not own their DOM range directly.
11
+
12
+ - Updated dependencies
13
+ [[`b54fdfc`](https://github.com/Ripple-TS/ripple/commit/b54fdfc3ebfea29ac613307b76732c5bf5f49ab5),
14
+ [`0a5f39b`](https://github.com/Ripple-TS/ripple/commit/0a5f39b6e13807dfd3dc1228f40d7bb02b933373),
15
+ [`165703c`](https://github.com/Ripple-TS/ripple/commit/165703c588b52f3dc0d26c06187f21700d448693)]:
16
+ - @tsrx/core@0.1.8
17
+ - ripple@0.3.58
18
+ - @tsrx/ripple@0.1.8
19
+
20
+ ## 0.3.57
21
+
22
+ ### Patch Changes
23
+
24
+ - [#1126](https://github.com/Ripple-TS/ripple/pull/1126)
25
+ [`2b1f746`](https://github.com/Ripple-TS/ripple/commit/2b1f7469ab31713140a5baf912a19fa8eedb9234)
26
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Keep runtime helper imports
27
+ on namespaced runtime subpaths so production app bundles do not pull in
28
+ compiler-only modules.
29
+
30
+ - [#1123](https://github.com/Ripple-TS/ripple/pull/1123)
31
+ [`e4a04dd`](https://github.com/Ripple-TS/ripple/commit/e4a04ddb4bbc8e21a9c7c2c65b179d764b72e4fb)
32
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Nested lazy destructuring
33
+ support for all tsrx targets. Ripple already fully supported it.
34
+ - Updated dependencies
35
+ [[`2b1f746`](https://github.com/Ripple-TS/ripple/commit/2b1f7469ab31713140a5baf912a19fa8eedb9234),
36
+ [`e4a04dd`](https://github.com/Ripple-TS/ripple/commit/e4a04ddb4bbc8e21a9c7c2c65b179d764b72e4fb)]:
37
+ - @tsrx/core@0.1.7
38
+ - ripple@0.3.57
39
+ - @tsrx/ripple@0.1.7
40
+
3
41
  ## 0.3.56
4
42
 
5
43
  ### Patch Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.3.56",
6
+ "version": "0.3.58",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -76,8 +76,8 @@
76
76
  "esm-env": "^1.2.2",
77
77
  "@types/estree": "^1.0.8",
78
78
  "@types/estree-jsx": "^1.0.5",
79
- "@tsrx/core": "0.1.6",
80
- "@tsrx/ripple": "0.1.6"
79
+ "@tsrx/core": "0.1.8",
80
+ "@tsrx/ripple": "0.1.8"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@types/node": "^24.3.0",
@@ -87,6 +87,6 @@
87
87
  "vscode-languageserver-types": "^3.17.5"
88
88
  },
89
89
  "peerDependencies": {
90
- "ripple": "0.3.56"
90
+ "ripple": "0.3.58"
91
91
  }
92
92
  }
@@ -188,7 +188,7 @@ export function root(fn, compat) {
188
188
  };
189
189
  }
190
190
 
191
- return block(ROOT_BLOCK, target_fn, { compat }, create_component_ctx());
191
+ return block(ROOT_BLOCK, target_fn, { compat, start: null, end: null }, create_component_ctx());
192
192
  }
193
193
 
194
194
  /**
@@ -274,7 +274,7 @@ export function destroy_block_children(parent, remove_dom = false) {
274
274
  var block = parent.first;
275
275
  parent.first = parent.last = null;
276
276
 
277
- if ((parent.f & CONTAINS_TEARDOWN) !== 0) {
277
+ if (remove_dom || (parent.f & CONTAINS_TEARDOWN) !== 0) {
278
278
  while (block !== null) {
279
279
  var next = block.next;
280
280
  destroy_block(block, remove_dom);
@@ -456,7 +456,7 @@ export function destroy_block(block, remove_dom = true) {
456
456
  (f & HEAD_BLOCK) !== 0
457
457
  ) {
458
458
  var s = block.s;
459
- if (s !== null) {
459
+ if (s !== null && s.start !== null) {
460
460
  remove_block_dom(s.start, s.end);
461
461
  removed = true;
462
462
  }
@@ -3,11 +3,11 @@
3
3
  * @typedef {EventTarget & Record<string, any>} DelegatedEventTarget
4
4
  */
5
5
  import {
6
- eventNameFromCapture as event_name_from_capture,
7
- isCaptureEvent as is_capture_event,
8
- isNonDelegated as is_non_delegated,
9
- isPassiveEvent as is_passive_event,
10
- } from '@tsrx/core';
6
+ event_name_from_capture,
7
+ is_capture_event,
8
+ is_non_delegated,
9
+ is_passive_event,
10
+ } from '@tsrx/core/runtime/events';
11
11
  import {
12
12
  active_block,
13
13
  active_reaction,
@@ -10,13 +10,10 @@ import {
10
10
  get_prototype_of,
11
11
  } from '@tsrx/core/runtime/language-helpers';
12
12
  import { event } from './events.js';
13
- import {
14
- getAttributeEventName as get_attribute_event_name,
15
- isEventAttribute as is_event_attribute,
16
- } from '@tsrx/core';
13
+ import { get_attribute_event_name, is_event_attribute } from '@tsrx/core/runtime/events';
17
14
  import { get } from './runtime.js';
18
15
  import { clsx } from 'clsx';
19
- import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
16
+ import { normalize_css_property_name } from '@tsrx/core/runtime/html';
20
17
 
21
18
  /**
22
19
  * @param {Text} text
@@ -29,10 +29,13 @@ import {
29
29
  import { DEV } from 'esm-env';
30
30
  import { is_ripple_object } from '../client/utils.js';
31
31
  import { array_slice } from '@tsrx/core/runtime/language-helpers';
32
- import { escape, escapeScript as escape_script } from '@tsrx/core';
33
- import { isBooleanAttribute as is_boolean_attribute } from '@tsrx/core';
32
+ import {
33
+ escape,
34
+ escape_script,
35
+ is_boolean_attribute,
36
+ normalize_css_property_name,
37
+ } from '@tsrx/core/runtime/html';
34
38
  import { clsx } from 'clsx';
35
- import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
36
39
  import { create_ref_prop } from '@tsrx/core/runtime/ref';
37
40
  import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
38
41
  import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js';
@@ -53,7 +56,7 @@ import { COMPONENT_BLOCK, TRY_BLOCK } from './constants.js';
53
56
 
54
57
  export { escape };
55
58
  export { register_component_css as register_css } from './css-registry.js';
56
- export { simpleHash as simple_hash, strongHash as strong_hash } from '@tsrx/core';
59
+ export { simple_hash, strong_hash } from '@tsrx/core/runtime/hash';
57
60
  export { context } from './context.js';
58
61
  export { try_block, component_block, regular_block } from './blocks.js';
59
62
  export { array_slice };
@@ -1,5 +1,6 @@
1
1
  import { HMR } from '../../../src/runtime/internal/client/constants.js';
2
2
  import { hmr } from '../../../src/runtime/internal/client/hmr.js';
3
+ import { effect, flushSync, mount } from 'ripple';
3
4
 
4
5
  describe('basic client > hmr', () => {
5
6
  it('handles updates before first render', () => {
@@ -16,4 +17,81 @@ describe('basic client > hmr', () => {
16
17
  expect(wrapper[HMR].current).toBeUndefined();
17
18
  expect(wrapper[HMR].fn).toBe(updated_component);
18
19
  });
20
+
21
+ it('removes DOM when an HMR-wrapped root is unmounted', () => {
22
+ component App() {
23
+ <div class="hmr-root">{'hmr root'}</div>
24
+ }
25
+
26
+ const cleanup = mount(hmr(App), { target: container });
27
+ flushSync();
28
+
29
+ expect(container.querySelector('.hmr-root')?.textContent).toBe('hmr root');
30
+
31
+ cleanup();
32
+
33
+ expect(container.querySelector('.hmr-root')).toBeNull();
34
+ expect(container.innerHTML).toBe('');
35
+ });
36
+
37
+ it('runs child teardowns when an HMR-wrapped root is unmounted', () => {
38
+ let teardown_count = 0;
39
+
40
+ component Child() {
41
+ effect(() => {
42
+ return () => {
43
+ teardown_count++;
44
+ };
45
+ });
46
+
47
+ <span class="hmr-child">{'child'}</span>
48
+ }
49
+
50
+ component App() {
51
+ <Child />
52
+ }
53
+
54
+ const cleanup = mount(hmr(App), { target: container });
55
+ flushSync();
56
+
57
+ expect(container.querySelector('.hmr-child')?.textContent).toBe('child');
58
+ expect(teardown_count).toBe(0);
59
+
60
+ cleanup();
61
+
62
+ expect(container.querySelector('.hmr-child')).toBeNull();
63
+ expect(teardown_count).toBe(1);
64
+ });
65
+
66
+ it('does not double-remove child DOM when the root owns the DOM range', () => {
67
+ let remove_count = 0;
68
+ const original_remove = Element.prototype.remove;
69
+
70
+ Element.prototype.remove = function () {
71
+ if (this.classList.contains('owned-root')) {
72
+ remove_count++;
73
+ }
74
+ return original_remove.call(this);
75
+ };
76
+
77
+ try {
78
+ component App() {
79
+ <div class="owned-root">
80
+ <span>{'child'}</span>
81
+ </div>
82
+ }
83
+
84
+ const cleanup = mount(App, { target: container });
85
+ flushSync();
86
+
87
+ expect(container.querySelector('.owned-root')).not.toBeNull();
88
+
89
+ cleanup();
90
+
91
+ expect(container.querySelector('.owned-root')).toBeNull();
92
+ expect(remove_count).toBe(1);
93
+ } finally {
94
+ Element.prototype.remove = original_remove;
95
+ }
96
+ });
19
97
  });
@@ -140,7 +140,11 @@ describe('lazy destructuring', () => {
140
140
  it(
141
141
  'preserves lazy getter/setter behavior for RestElement nested destructuring in non-lazy component params',
142
142
  async () => {
143
- component Inner({ values: [head, ...&{ 0: first_rest, length: rest_length }] }) {
143
+ component Inner({
144
+ values: [head, ...&{ 0: first_rest, length: rest_length }],
145
+ }: {
146
+ values: number[];
147
+ }) {
144
148
  const before = `${first_rest}-${rest_length}`;
145
149
  rest_length = 0;
146
150
  <pre>{`${head}-${before}-${first_rest}-${rest_length}`}</pre>
@@ -159,7 +163,11 @@ describe('lazy destructuring', () => {
159
163
  'preserves lazy getter/setter behavior for RestElement nested destructuring in non-lazy function params',
160
164
  async () => {
161
165
  component Test() {
162
- function getInfo({ values: [head, ...&{ 0: first_rest, length: rest_length }] }) {
166
+ function getInfo({
167
+ values: [head, ...&{ 0: first_rest, length: rest_length }],
168
+ }: {
169
+ values: number[];
170
+ }) {
163
171
  const before = `${first_rest}-${rest_length}`;
164
172
  rest_length = 0;
165
173
  return `${head}-${before}-${first_rest}-${rest_length}`;
@@ -228,4 +236,304 @@ describe('lazy destructuring', () => {
228
236
  const { body } = await render(Test);
229
237
  expect(body).toBeHtml('<pre>10-20</pre>');
230
238
  });
239
+
240
+ describe('nested lazy destructuring', () => {
241
+ it('preserves nested lazy object access inside lazy object as component params', async () => {
242
+ let inner_value = 7;
243
+
244
+ component Inner(&{ outer: &{ inner } }: { outer: { inner: number } }) {
245
+ const before = inner;
246
+ inner_value = 8;
247
+ <pre>{`${before}-${inner}`}</pre>
248
+ }
249
+
250
+ component Test() {
251
+ const outer = {
252
+ get inner() {
253
+ return inner_value;
254
+ },
255
+ };
256
+ <Inner {outer} />
257
+ }
258
+
259
+ const { body } = await render(Test);
260
+ expect(body).toBeHtml('<pre>7-8</pre>');
261
+ });
262
+
263
+ it('preserves nested lazy array access inside regular object as component params', async () => {
264
+ let first_value = 3;
265
+ let second_value = 4;
266
+
267
+ component Inner({ pair: &[first, second] }: { pair: [number, number] }) {
268
+ const before = `${first}-${second}`;
269
+ first_value = 5;
270
+ second_value = 6;
271
+ <pre>{`${before}-${first}-${second}`}</pre>
272
+ }
273
+
274
+ component Test() {
275
+ const pair = [0, 0] as [number, number];
276
+ Object.defineProperty(pair, 0, { get: () => first_value });
277
+ Object.defineProperty(pair, 1, { get: () => second_value });
278
+ <Inner {pair} />
279
+ }
280
+
281
+ const { body } = await render(Test);
282
+ expect(body).toBeHtml('<pre>3-4-5-6</pre>');
283
+ });
284
+
285
+ it('preserves nested lazy object access inside lazy array as function params', async () => {
286
+ component Test() {
287
+ let name_value = 'Alice';
288
+ function getName(&[&{ name }]: [{ name: string }]) {
289
+ const before = name;
290
+ name_value = 'Bob';
291
+ return `${before}-${name}`;
292
+ }
293
+ const user = {
294
+ get name() {
295
+ return name_value;
296
+ },
297
+ };
298
+ <pre>{getName([user])}</pre>
299
+ }
300
+
301
+ const { body } = await render(Test);
302
+ expect(body).toBeHtml('<pre>Alice-Bob</pre>');
303
+ });
304
+
305
+ it('preserves nested lazy array access inside lazy array as function params', async () => {
306
+ component Test() {
307
+ let first_value = 5;
308
+ let second_value = 6;
309
+ function getValues(&[&[a, b]]: [[number, number]]) {
310
+ const before = `${a}-${b}`;
311
+ first_value = 7;
312
+ second_value = 8;
313
+ return `${before}-${a}-${b}`;
314
+ }
315
+ const pair = [0, 0] as [number, number];
316
+ Object.defineProperty(pair, 0, { get: () => first_value });
317
+ Object.defineProperty(pair, 1, { get: () => second_value });
318
+ <pre>{getValues([pair])}</pre>
319
+ }
320
+
321
+ const { body } = await render(Test);
322
+ expect(body).toBeHtml('<pre>5-6-7-8</pre>');
323
+ });
324
+
325
+ it('preserves three-level lazy object access as component params', async () => {
326
+ let c_value = 42;
327
+
328
+ component Inner(&{ a: &{ b: &{ c } } }: { a: { b: { c: number } } }) {
329
+ const before = c;
330
+ c_value = 43;
331
+ <pre>{`${before}-${c}`}</pre>
332
+ }
333
+
334
+ component Test() {
335
+ const a = {
336
+ b: {
337
+ get c() {
338
+ return c_value;
339
+ },
340
+ },
341
+ };
342
+ <Inner {a} />
343
+ }
344
+
345
+ const { body } = await render(Test);
346
+ expect(body).toBeHtml('<pre>42-43</pre>');
347
+ });
348
+
349
+ it('preserves nested lazy object access inside lazy object as function params', async () => {
350
+ component Test() {
351
+ let inner_value = 11;
352
+ function getValue(&{ outer: &{ inner } }: { outer: { inner: number } }) {
353
+ const before = inner;
354
+ inner_value = 12;
355
+ return `${before}-${inner}`;
356
+ }
357
+ const outer = {
358
+ get inner() {
359
+ return inner_value;
360
+ },
361
+ };
362
+ <pre>{getValue({ outer })}</pre>
363
+ }
364
+
365
+ const { body } = await render(Test);
366
+ expect(body).toBeHtml('<pre>11-12</pre>');
367
+ });
368
+
369
+ it(
370
+ 'supports nested lazy array inside lazy object as function params with writeback',
371
+ async () => {
372
+ component Test() {
373
+ const obj = { pair: [1, 2] as [number, number] };
374
+ function bump(&{ pair: &[first, second] }: { pair: [number, number] }) {
375
+ first = first + 10;
376
+ second = second + 20;
377
+ }
378
+ bump(obj);
379
+ <pre>{`${obj.pair[0]}-${obj.pair[1]}`}</pre>
380
+ }
381
+
382
+ const { body } = await render(Test);
383
+ expect(body).toBeHtml('<pre>11-22</pre>');
384
+ },
385
+ );
386
+
387
+ it('preserves three-level lazy object access as function params', async () => {
388
+ component Test() {
389
+ let c_value = 99;
390
+ function getValue(&{ a: &{ b: &{ c } } }: { a: { b: { c: number } } }) {
391
+ const before = c;
392
+ c_value = 100;
393
+ return `${before}-${c}`;
394
+ }
395
+ const a = {
396
+ b: {
397
+ get c() {
398
+ return c_value;
399
+ },
400
+ },
401
+ };
402
+ <pre>{getValue({ a })}</pre>
403
+ }
404
+
405
+ const { body } = await render(Test);
406
+ expect(body).toBeHtml('<pre>99-100</pre>');
407
+ });
408
+
409
+ it('preserves nested lazy object access inside lazy object in const declaration', async () => {
410
+ component Test() {
411
+ let inner_value = 5;
412
+ const data = {
413
+ outer: {
414
+ get inner() {
415
+ return inner_value;
416
+ },
417
+ },
418
+ };
419
+ const &{ outer: &{ inner } } = data;
420
+ const before = inner;
421
+ inner_value = 6;
422
+ <pre>{`${before}-${inner}`}</pre>
423
+ }
424
+
425
+ const { body } = await render(Test);
426
+ expect(body).toBeHtml('<pre>5-6</pre>');
427
+ });
428
+
429
+ it('supports nested lazy object inside lazy object in let with writeback', async () => {
430
+ component Test() {
431
+ const data = { outer: { inner: 5 } };
432
+ let &{ outer: &{ inner } } = data;
433
+ inner = 50;
434
+ <pre>{data.outer.inner}</pre>
435
+ }
436
+
437
+ const { body } = await render(Test);
438
+ expect(body).toBeHtml('<pre>50</pre>');
439
+ });
440
+
441
+ it('supports nested lazy array inside lazy object in let with writeback', async () => {
442
+ component Test() {
443
+ const data = { pair: [1, 2] as [number, number] };
444
+ let &{ pair: &[first, second] } = data;
445
+ first = 100;
446
+ second = 200;
447
+ <pre>{`${data.pair[0]}-${data.pair[1]}`}</pre>
448
+ }
449
+
450
+ const { body } = await render(Test);
451
+ expect(body).toBeHtml('<pre>100-200</pre>');
452
+ });
453
+
454
+ it('supports three-level lazy object nesting in let with writeback', async () => {
455
+ component Test() {
456
+ const data = { a: { b: { c: 1 } } };
457
+ let &{ a: &{ b: &{ c } } } = data;
458
+ c = 999;
459
+ <pre>{data.a.b.c}</pre>
460
+ }
461
+
462
+ const { body } = await render(Test);
463
+ expect(body).toBeHtml('<pre>999</pre>');
464
+ });
465
+
466
+ it('supports compound assignment on deeply nested lazy bindings', async () => {
467
+ component Test() {
468
+ const data = { a: { b: { c: 5 } } };
469
+ let &{ a: &{ b: &{ c } } } = data;
470
+ c += 10;
471
+ c *= 2;
472
+ <pre>{data.a.b.c}</pre>
473
+ }
474
+
475
+ const { body } = await render(Test);
476
+ expect(body).toBeHtml('<pre>30</pre>');
477
+ });
478
+
479
+ it('preserves default values inside nested lazy destructuring', async () => {
480
+ component Test() {
481
+ let inner_value: number | undefined;
482
+ const data: { outer: { inner?: number } } = {
483
+ outer: {
484
+ get inner() {
485
+ return inner_value;
486
+ },
487
+ },
488
+ };
489
+ const &{ outer: &{ inner = 42 } } = data;
490
+ const before = inner;
491
+ inner_value = 43;
492
+ <pre>{`${before}-${inner}`}</pre>
493
+ }
494
+
495
+ const { body } = await render(Test);
496
+ expect(body).toBeHtml('<pre>42-43</pre>');
497
+ });
498
+
499
+ it('preserves multiple sibling nested lazy destructures', async () => {
500
+ component Test() {
501
+ let x_value = 1;
502
+ let y_value = 2;
503
+ const data = {
504
+ a: {
505
+ get x() {
506
+ return x_value;
507
+ },
508
+ },
509
+ b: {
510
+ get y() {
511
+ return y_value;
512
+ },
513
+ },
514
+ };
515
+ const &{ a: &{ x }, b: &{ y } } = data;
516
+ const before = `${x}-${y}`;
517
+ x_value = 3;
518
+ y_value = 4;
519
+ <pre>{`${before}-${x}-${y}`}</pre>
520
+ }
521
+
522
+ const { body } = await render(Test);
523
+ expect(body).toBeHtml('<pre>1-2-3-4</pre>');
524
+ });
525
+
526
+ it('supports multiple sibling nested lazy destructures with writeback', async () => {
527
+ component Test() {
528
+ const data = { a: { x: 1 }, b: { y: 2 } };
529
+ let &{ a: &{ x }, b: &{ y } } = data;
530
+ x = 11;
531
+ y = 22;
532
+ <pre>{`${data.a.x}-${data.b.y}`}</pre>
533
+ }
534
+
535
+ const { body } = await render(Test);
536
+ expect(body).toBeHtml('<pre>11-22</pre>');
537
+ });
538
+ });
231
539
  });
@@ -0,0 +1,38 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const runtime_dir = new URL('../../src/runtime/', import.meta.url);
6
+
7
+ /** @param {string} dir */
8
+ async function get_js_files(dir) {
9
+ const entries = await readdir(dir, { withFileTypes: true });
10
+ const files = [];
11
+
12
+ for (const entry of entries) {
13
+ const path = join(dir, entry.name);
14
+ if (entry.isDirectory()) {
15
+ files.push(...(await get_js_files(path)));
16
+ } else if (entry.isFile() && entry.name.endsWith('.js')) {
17
+ files.push(path);
18
+ }
19
+ }
20
+
21
+ return files;
22
+ }
23
+
24
+ describe('runtime imports', () => {
25
+ it('does not import the @tsrx/core compiler barrel', async () => {
26
+ const files = await get_js_files(runtime_dir.pathname);
27
+ const barrel_imports = [];
28
+
29
+ for (const file of files) {
30
+ const source = await readFile(file, 'utf8');
31
+ if (/from\s+['"]@tsrx\/core['"]/.test(source)) {
32
+ barrel_imports.push(file);
33
+ }
34
+ }
35
+
36
+ expect(barrel_imports).toEqual([]);
37
+ });
38
+ });