ripple 0.3.74 → 0.3.76

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,55 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.76
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1229](https://github.com/Ripple-TS/ripple/pull/1229)
8
+ [`6fd49c9`](https://github.com/Ripple-TS/ripple/commit/6fd49c9dd737e889844e254763f66e13ea4a7241)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Replace the removed `<@...>`
10
+ dynamic tag syntax with runtime `Dynamic` helpers. Ripple now exports `Dynamic`
11
+ and reuses its composite runtime path for dynamic elements/components, while
12
+ React, Preact, Solid, and Vue expose target-specific `Dynamic` helpers with
13
+ typed `is` props.
14
+
15
+ React, Preact, Solid, and Vue now mark imported runtime `Dynamic` elements
16
+ during shared JSX analysis so scoped CSS classes are applied through aliases
17
+ without treating local components named `Dynamic` as runtime elements.
18
+
19
+ Dynamic component prop forwarding now uses a shared core runtime helper that
20
+ excludes the internal `is` prop without snapshotting getter-backed reactive
21
+ props.
22
+
23
+ The TSRX parser, transforms, analyzers, prettier support, and related tests no
24
+ longer recognize dynamic tag syntax. Stale JSX identifier `tracked` plumbing
25
+ from that parser path has also been removed.
26
+
27
+ - Updated dependencies
28
+ [[`6fd49c9`](https://github.com/Ripple-TS/ripple/commit/6fd49c9dd737e889844e254763f66e13ea4a7241)]:
29
+ - @tsrx/core@0.1.24
30
+ - @tsrx/ripple@0.1.24
31
+
32
+ ## 0.3.75
33
+
34
+ ### Patch Changes
35
+
36
+ - Updated dependencies
37
+ [[`9eb4819`](https://github.com/Ripple-TS/ripple/commit/9eb4819cede6da7e93cbcd2bdf284bcb42d40464),
38
+ [`88a254c`](https://github.com/Ripple-TS/ripple/commit/88a254c69953a5ace33bc10047f11052ec598672),
39
+ [`ba3a7f6`](https://github.com/Ripple-TS/ripple/commit/ba3a7f6485ea163e60cc0750a8e8b06b50728009),
40
+ [`ac6f358`](https://github.com/Ripple-TS/ripple/commit/ac6f3582ca0b2814004439c882d6aa735c8afe50),
41
+ [`4c5f992`](https://github.com/Ripple-TS/ripple/commit/4c5f992b9a11e1f26abee476a6add89f959169bc),
42
+ [`78ffa8d`](https://github.com/Ripple-TS/ripple/commit/78ffa8d90fd01e85bf34e5c6adef0e51caae8da7),
43
+ [`16560cb`](https://github.com/Ripple-TS/ripple/commit/16560cb466430bdbe8749d9491bc79e69e58d02c),
44
+ [`186b3b2`](https://github.com/Ripple-TS/ripple/commit/186b3b2557761ff06c9056bf2e0b7ab8c7692477),
45
+ [`4be6e54`](https://github.com/Ripple-TS/ripple/commit/4be6e54bbfee20927adca473648a94aa173d7d77),
46
+ [`2b67f83`](https://github.com/Ripple-TS/ripple/commit/2b67f83d7ed7eab7a39bc33524fcf73f737d977e),
47
+ [`9918c52`](https://github.com/Ripple-TS/ripple/commit/9918c52e954f2b8e1a994892e7c555e8277f2d59),
48
+ [`e8493be`](https://github.com/Ripple-TS/ripple/commit/e8493be0b3489f402105297251e1919c103c2360),
49
+ [`c424675`](https://github.com/Ripple-TS/ripple/commit/c424675102a9edd4f1e356fb6db30124a9c2d885)]:
50
+ - @tsrx/core@0.1.23
51
+ - @tsrx/ripple@0.1.23
52
+
3
53
  ## 0.3.74
4
54
 
5
55
  ### 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.74",
6
+ "version": "0.3.76",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -74,8 +74,8 @@
74
74
  "clsx": "^2.1.1",
75
75
  "devalue": "^5.8.1",
76
76
  "esm-env": "^1.2.2",
77
- "@tsrx/core": "0.1.22",
78
- "@tsrx/ripple": "0.1.22"
77
+ "@tsrx/core": "0.1.24",
78
+ "@tsrx/ripple": "0.1.24"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@types/estree": "^1.0.8",
@@ -8,8 +8,8 @@ export type { RefValue } from '#public';
8
8
  * renderable TSRX values when used in expression positions.
9
9
  */
10
10
 
11
- // Ripple components don't return JSX elements - they're imperative
12
- export type ComponentType<P = {}> = (props: P) => void;
11
+ // Ripple components are usually imperative, but helpers can return TSRX values.
12
+ export type ComponentType<P = {}> = (props: P) => void | TSRXElement;
13
13
 
14
14
  /**
15
15
  * Create a JSX element (for elements with children)
@@ -0,0 +1,33 @@
1
+ /** @import { Block } from '#client' */
2
+
3
+ import { composite } from './internal/client/composite.js';
4
+ import { with_block } from './internal/client/runtime.js';
5
+ import { tsrx_element } from './element.js';
6
+
7
+ /**
8
+ * @typedef {Function | string | null | undefined | false} DynamicTarget
9
+ * @typedef {{ is?: DynamicTarget, [key: string]: any }} DynamicProps
10
+ */
11
+
12
+ /**
13
+ * @param {DynamicProps} props
14
+ * @returns {import('./element.js').TSRXElement}
15
+ */
16
+ export function Dynamic(props) {
17
+ return tsrx_element(
18
+ /**
19
+ * @param {Node} anchor
20
+ * @param {Block | null} block
21
+ */
22
+ (anchor, block) => {
23
+ const render_dynamic = () =>
24
+ composite(() => /** @type {DynamicTarget} */ (props?.is), anchor, props || {}, 'is');
25
+
26
+ if (block !== null) {
27
+ with_block(block, render_dynamic);
28
+ } else {
29
+ render_dynamic();
30
+ }
31
+ },
32
+ );
33
+ }
@@ -0,0 +1,80 @@
1
+ import { is_void_element } from '@tsrx/core/runtime/html';
2
+ import { exclude_prop_from_object } from '@tsrx/core/runtime/language-helpers';
3
+ import {
4
+ escape,
5
+ get,
6
+ is_tsrx_element,
7
+ output_push,
8
+ render_component,
9
+ render_tsrx_element,
10
+ spread_attrs,
11
+ spread_inner_html,
12
+ } from './internal/server/index.js';
13
+ import { tsrx_element } from './element.js';
14
+
15
+ /**
16
+ * @param {any} value
17
+ * @returns {void}
18
+ */
19
+ function render_child(value) {
20
+ value = get(value);
21
+
22
+ if (is_tsrx_element(value)) {
23
+ render_tsrx_element(value);
24
+ } else if (Array.isArray(value)) {
25
+ for (const item of value) {
26
+ render_child(item);
27
+ }
28
+ } else if (value != null) {
29
+ output_push(escape(value));
30
+ }
31
+ }
32
+
33
+ /**
34
+ * @param {string} tag
35
+ * @param {Record<string, any>} props
36
+ * @returns {void}
37
+ */
38
+ function render_element(tag, props) {
39
+ output_push(`<${tag}`);
40
+ output_push(spread_attrs(props, undefined, 'is'));
41
+
42
+ if (is_void_element(tag)) {
43
+ output_push(' />');
44
+ return;
45
+ }
46
+
47
+ output_push('>');
48
+
49
+ const inner_html = spread_inner_html(props);
50
+ if (inner_html !== undefined) {
51
+ output_push(inner_html);
52
+ } else {
53
+ render_child(props.children);
54
+ }
55
+
56
+ output_push(`</${tag}>`);
57
+ }
58
+
59
+ /**
60
+ * @param {{ is?: Function | string | null | undefined | false, [key: string]: any }} props
61
+ * @returns {import('./element.js').TSRXElement}
62
+ */
63
+ export function Dynamic(props) {
64
+ return tsrx_element(() => {
65
+ const component = get(props?.is);
66
+ if (component == null || component === false) {
67
+ return;
68
+ }
69
+
70
+ const dynamic_props = props || {};
71
+
72
+ if (typeof component === 'function') {
73
+ render_component(component, exclude_prop_from_object(dynamic_props, 'is'));
74
+ } else if (is_tsrx_element(component)) {
75
+ throw new TypeError('Invalid component type: received a TSRXElement value.');
76
+ } else {
77
+ render_element(String(component), dynamic_props);
78
+ }
79
+ });
80
+ }
@@ -183,6 +183,8 @@ export { user_effect as effect } from './internal/client/blocks.js';
183
183
 
184
184
  export { Portal } from './internal/client/portal.js';
185
185
 
186
+ export { Dynamic } from './dynamic-client.js';
187
+
186
188
  export { ref_prop as createRefKey } from './internal/client/runtime.js';
187
189
 
188
190
  export { isRefProp } from '@tsrx/core/runtime/ref';
@@ -68,6 +68,8 @@ export const bindNode = noop;
68
68
  export const bindOffsetWidth = noop;
69
69
  export const bindOffsetHeight = noop;
70
70
 
71
+ export { Dynamic } from './dynamic-server.js';
72
+
71
73
  /**
72
74
  * Portal component noop for server-side rendering.
73
75
  * Portals are client-only and do not render on the server.
@@ -85,9 +85,10 @@ export function render(fn, state, flags = 0) {
85
85
  * @param {any} element
86
86
  * @param {any} fn
87
87
  * @param {number} [flags]
88
+ * @param {string} [exclude_prop]
88
89
  */
89
- export function render_spread(element, fn, flags = 0) {
90
- return block(RENDER_BLOCK | flags, apply_element_spread(element, fn));
90
+ export function render_spread(element, fn, flags = 0, exclude_prop) {
91
+ return block(RENDER_BLOCK | flags, apply_element_spread(element, fn, exclude_prop));
91
92
  }
92
93
 
93
94
  /**
@@ -1,5 +1,6 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
+ import { exclude_prop_from_object } from '@tsrx/core/runtime/language-helpers';
3
4
  import { branch, destroy_block, render, render_spread } from './blocks.js';
4
5
  import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
5
6
  import { hydrate_next, hydrating } from './hydration.js';
@@ -9,13 +10,14 @@ import { is_tsrx_element } from '../../element.js';
9
10
  import { render_component } from './component.js';
10
11
 
11
12
  /**
12
- * @typedef {((anchor: Node, props: Record<string, any>, block: Block | null) => void)} ComponentFunction
13
- * @param {() => ComponentFunction | keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap | keyof MathMLElementTagNameMap} get_component
13
+ * @typedef {Function | string | null | undefined | false} CompositeTarget
14
+ * @param {() => CompositeTarget} get_component
14
15
  * @param {Node} node
15
16
  * @param {Record<string, any>} props
17
+ * @param {string} [exclude_prop]
16
18
  * @returns {void}
17
19
  */
18
- export function composite(get_component, node, props) {
20
+ export function composite(get_component, node, props, exclude_prop) {
19
21
  if (hydrating) {
20
22
  // During hydration, `node` may already point at the first real SSR node
21
23
  // (e.g. layout children). Only skip forward when we are on an empty
@@ -42,7 +44,10 @@ export function composite(get_component, node, props) {
42
44
  if (typeof component === 'function') {
43
45
  // Handle as regular component
44
46
  b = branch(() => {
45
- render_component(component, anchor, props);
47
+ const component_props = exclude_prop
48
+ ? exclude_prop_from_object(props, exclude_prop)
49
+ : props;
50
+ render_component(component, anchor, component_props);
46
51
  });
47
52
  } else if (is_tsrx_element(component)) {
48
53
  throw new TypeError('Invalid component type: received a TSRXElement value.');
@@ -69,7 +74,7 @@ export function composite(get_component, node, props) {
69
74
  };
70
75
  }
71
76
 
72
- render_spread(element, () => props || {});
77
+ render_spread(element, () => props || {}, 0, exclude_prop);
73
78
 
74
79
  if (is_tsrx_element(props?.children)) {
75
80
  var child_anchor = document.createComment('');
@@ -84,7 +89,7 @@ export function composite(get_component, node, props) {
84
89
  };
85
90
 
86
91
  if (ns !== active_namespace) {
87
- // support top-level dynamic element svg/math <@tag />
92
+ // support top-level dynamic element svg/math tags
88
93
  b = branch(() => with_ns(ns, run));
89
94
  } else {
90
95
  b = branch(run);
@@ -253,9 +253,10 @@ export function set_selected(element, selected) {
253
253
  /**
254
254
  * @param {Element} element
255
255
  * @param {() => Record<string | symbol, any>} fn
256
+ * @param {string} [exclude_prop]
256
257
  * @returns {() => void}
257
258
  */
258
- export function apply_element_spread(element, fn) {
259
+ export function apply_element_spread(element, fn, exclude_prop) {
259
260
  /** @type {Record<string | symbol, any>} */
260
261
  var prev = {};
261
262
  /** @type {Record<string | symbol, Block | undefined>} */
@@ -304,6 +305,8 @@ export function apply_element_spread(element, fn) {
304
305
  var current_ref_props = {};
305
306
 
306
307
  for (const key in next) {
308
+ if (key === exclude_prop) continue;
309
+
307
310
  const ref_fn = next[key];
308
311
  if (!is_ref_prop(ref_fn)) {
309
312
  continue;
@@ -352,7 +355,7 @@ export function apply_element_spread(element, fn) {
352
355
  /** @type {typeof prev} */
353
356
  const current = {};
354
357
  for (const key in next) {
355
- if (key === 'children') continue;
358
+ if (key === 'children' || key === exclude_prop) continue;
356
359
 
357
360
  let value = next[key];
358
361
  if (is_ref_prop(value)) {
@@ -1244,18 +1244,25 @@ function get_styles(styles) {
1244
1244
  /**
1245
1245
  * @param {Record<string, any>} attrs
1246
1246
  * @param {string | undefined} css_hash
1247
+ * @param {string} [exclude_prop]
1247
1248
  * @returns {string}
1248
1249
  */
1249
- export function spread_attrs(attrs, css_hash) {
1250
+ export function spread_attrs(attrs, css_hash, exclude_prop) {
1250
1251
  let attr_str = '';
1251
1252
  let name;
1252
1253
 
1254
+ if (css_hash === undefined && Object.prototype.hasOwnProperty.call(attrs, '#class')) {
1255
+ css_hash = attrs['#class'];
1256
+ }
1257
+
1253
1258
  for (name in attrs) {
1254
1259
  var value = attrs[name];
1255
1260
 
1256
1261
  if (
1257
1262
  name === 'children' ||
1258
1263
  name === 'innerHTML' ||
1264
+ name === '#class' ||
1265
+ name === exclude_prop ||
1259
1266
  typeof value === 'function' ||
1260
1267
  is_tsrx_element(value)
1261
1268
  )
@@ -5,7 +5,7 @@ import type {
5
5
  Component,
6
6
  PropsWithChildrenOptional,
7
7
  } from 'ripple';
8
- import { flushSync, track } from 'ripple';
8
+ import { Dynamic, flushSync, track } from 'ripple';
9
9
  import { did_error } from '../capture-error.js';
10
10
 
11
11
  describe('basic client > components & composition', () => {
@@ -503,7 +503,7 @@ describe('basic client > components & composition', () => {
503
503
  function App() @{
504
504
  let &[Content] = track(() => Noop);
505
505
  <>
506
- <@Content />
506
+ <Dynamic is={Content} />
507
507
  <button onClick={() => (Content = Op)}>{'Show Op'}</button>
508
508
  </>
509
509
  }
@@ -39,23 +39,25 @@ describe('basic client > styling', () => {
39
39
  });
40
40
 
41
41
  it('renders with keyframes in styling scoped to component', () => {
42
- const source = `export function Basic() { return <>
43
- <div>
44
- <p>{'Styled paragraph'}</p>
45
- </div>
42
+ const source = `export function Basic() @{
43
+ <>
44
+ <div>
45
+ <p>{'Styled paragraph'}</p>
46
+ </div>
46
47
 
47
- <style>
48
- div {
49
- animation-name: anim;
50
- }
48
+ <style>
49
+ div {
50
+ animation-name: anim;
51
+ }
51
52
 
52
- @keyframes anim {}
53
+ @keyframes anim {}
53
54
 
54
- p {
55
- animation-name: anim;
56
- }
57
- </style>
58
- </>; }`;
55
+ p {
56
+ animation-name: anim;
57
+ }
58
+ </style>
59
+ </>
60
+ }`;
59
61
 
60
62
  const { css } = compile(source, 'test.tsrx');
61
63
  const name = css.match(/@keyframes\s+([a-zA-Z0-9_-]+)\s*\{/)[1];
@@ -1,14 +1,14 @@
1
- import { flushSync, track } from 'ripple';
1
+ import { Dynamic, flushSync, track } from 'ripple';
2
2
 
3
3
  describe('composite > dynamic components', () => {
4
- it('supports rendering composite components using <@component> syntax', () => {
4
+ it('supports rendering composite components using <Dynamic is={component}> syntax', () => {
5
5
  function basic() @{
6
6
  <div>{'Basic Component'}</div>
7
7
  }
8
8
 
9
9
  function App() @{
10
10
  const tracked_basic = track(() => basic);
11
- <@tracked_basic />
11
+ <Dynamic is={tracked_basic} />
12
12
  }
13
13
 
14
14
  render(App);
@@ -28,7 +28,7 @@ describe('composite > dynamic components', () => {
28
28
  tracked_basic,
29
29
  };
30
30
  const comp = obj.tracked_basic;
31
- <@comp />
31
+ <Dynamic is={comp} />
32
32
  }
33
33
 
34
34
  render(App);
@@ -49,7 +49,7 @@ describe('composite > dynamic components', () => {
49
49
  };
50
50
  let &[inner] = track(obj);
51
51
  const comp = inner.tracked_basic;
52
- <@comp />
52
+ <Dynamic is={comp} />
53
53
  }
54
54
 
55
55
  render(App);
@@ -58,6 +58,24 @@ describe('composite > dynamic components', () => {
58
58
  expect(container.textContent).toBe('Basic Component');
59
59
  });
60
60
 
61
+ it('does not pass is to dynamic component props', () => {
62
+ function Child(props) @{
63
+ <div>
64
+ {props.is === undefined && !('is' in props) ? 'hidden' : 'leaked'}
65
+ </div>
66
+ }
67
+
68
+ function App() @{
69
+ const component = track(() => Child);
70
+ <Dynamic is={component} label="child" />
71
+ }
72
+
73
+ render(App);
74
+ flushSync();
75
+
76
+ expect(container.textContent).toBe('hidden');
77
+ });
78
+
61
79
  it('handles dynamic component switching', () => {
62
80
  function Child1() @{
63
81
  <div>{'I am child 1'}</div>
@@ -71,7 +89,7 @@ describe('composite > dynamic components', () => {
71
89
  let &[thing] = track(() => Child1);
72
90
  <>
73
91
  <div id="container">
74
- <@thing />
92
+ <Dynamic is={thing} />
75
93
  </div>
76
94
  <button
77
95
  onClick={() => (thing = thing === Child1 ? Child2 : Child1)}
@@ -463,7 +463,7 @@ function Child1() @{
463
463
  'handles sibling combinators with dynamic component and :global before scoped elements',
464
464
  () => {
465
465
  const source = `
466
- import { track } from 'ripple';
466
+ import { Dynamic, track } from 'ripple';
467
467
 
468
468
  export function Test({ children }) @{
469
469
  const DynamicComponent = track(() => Child1);
@@ -471,7 +471,7 @@ export function Test({ children }) @{
471
471
  <div>
472
472
  <p class="before">{'before'}</p>
473
473
 
474
- <@DynamicComponent />
474
+ <Dynamic is={DynamicComponent} />
475
475
 
476
476
  <p class="foo">
477
477
  <span>{'foo'}</span>
@@ -512,7 +512,7 @@ function Child1() @{
512
512
  'handles sibling combinators with dynamic element or regular element and :global before scoped elements',
513
513
  () => {
514
514
  const source = `
515
- import { track } from 'ripple';
515
+ import { Dynamic, track } from 'ripple';
516
516
 
517
517
  export function Test({ children, classes }) @{
518
518
  const dynamicElement = track('div');
@@ -520,7 +520,7 @@ export function Test({ children, classes }) @{
520
520
  <div>
521
521
  <p class="before">{'before'}</p>
522
522
  // Use Dynamic Element but it's the same with a regular one
523
- <@dynamicElement class={classes} />
523
+ <Dynamic is={dynamicElement} class={classes} />
524
524
 
525
525
  <p class="foo">
526
526
  <span>{'foo'}</span>
@@ -1,4 +1,4 @@
1
- import { track } from 'ripple';
1
+ import { Dynamic, track } from 'ripple';
2
2
  import { compile } from '@tsrx/ripple';
3
3
 
4
4
  const external_styles = <style>
@@ -75,7 +75,7 @@ describe('style class maps', () => {
75
75
  it('allows style expression classes on child components with children', () => {
76
76
  const source = `
77
77
  function Child({ className }) @{
78
- <div class={className}>"hello world"</div>
78
+ <div class={className}>hello world</div>
79
79
  }
80
80
  function App() @{
81
81
  const styles = <style>
@@ -84,7 +84,7 @@ function App() @{
84
84
  }
85
85
  </style>;
86
86
 
87
- <Child className={styles.container}>"hello world"</Child>
87
+ <Child className={styles.container}>hello world</Child>
88
88
  }`;
89
89
 
90
90
  expect(() => compile(source, 'test.tsrx')).not.toThrow();
@@ -104,7 +104,7 @@ function App() @{
104
104
 
105
105
  let dynamic = track(() => Child);
106
106
  <div class="wrapper">
107
- <@dynamic cls={styles.text} />
107
+ <Dynamic is={dynamic} cls={styles.text} />
108
108
  </div>
109
109
  }
110
110