ripple 0.2.143 → 0.2.145

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/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.2.143",
6
+ "version": "0.2.145",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -81,6 +81,6 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "ripple": "0.2.143"
84
+ "ripple": "0.2.145"
85
85
  }
86
86
  }
@@ -491,6 +491,7 @@ function RipplePlugin(config) {
491
491
  forInit,
492
492
  );
493
493
  }
494
+
494
495
  /**
495
496
  * Parse expression atom - handles TrackedArray and TrackedObject literals
496
497
  * @param {any} [refDestructuringErrors]
@@ -524,6 +525,11 @@ function RipplePlugin(config) {
524
525
  return this.parseTrackedObjectExpression();
525
526
  }
526
527
 
528
+ // Check if this is a component expression (e.g., in object literal values)
529
+ if (this.type === tt.name && this.value === 'component') {
530
+ return this.parseComponent();
531
+ }
532
+
527
533
  return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
528
534
  }
529
535
 
@@ -714,33 +720,55 @@ function RipplePlugin(config) {
714
720
  return this.finishNode(node, 'TrackedObjectExpression');
715
721
  }
716
722
 
717
- parseExportDefaultDeclaration() {
718
- // Check if this is "export default component"
719
- if (this.value === 'component') {
720
- const node = this.startNode();
721
- node.type = 'Component';
722
- node.css = null;
723
- node.default = true;
724
- this.next();
725
- this.enterScope(0);
723
+ /**
724
+ * Parse a component - common implementation used by statements, expressions, and export defaults
725
+ * @param {Object} options - Parsing options
726
+ * @param {boolean} [options.requireName=false] - Whether component name is required
727
+ * @param {boolean} [options.isDefault=false] - Whether this is an export default component
728
+ * @param {boolean} [options.declareName=false] - Whether to declare the name in scope
729
+ * @returns {any} Component node
730
+ */
731
+ parseComponent({ requireName = false, isDefault = false, declareName = false } = {}) {
732
+ const node = this.startNode();
733
+ node.type = 'Component';
734
+ node.css = null;
735
+ node.default = isDefault;
736
+ this.next(); // consume 'component'
737
+ this.enterScope(0);
726
738
 
739
+ if (requireName) {
740
+ node.id = this.parseIdent();
741
+ if (declareName) {
742
+ this.declareName(node.id.name, 'var', node.id.start);
743
+ }
744
+ } else {
727
745
  node.id = this.type.label === 'name' ? this.parseIdent() : null;
746
+ if (declareName && node.id) {
747
+ this.declareName(node.id.name, 'var', node.id.start);
748
+ }
749
+ }
728
750
 
729
- this.parseFunctionParams(node);
730
- this.eat(tt.braceL);
731
- node.body = [];
732
- this.#path.push(node);
751
+ this.parseFunctionParams(node);
752
+ this.eat(tt.braceL);
753
+ node.body = [];
754
+ this.#path.push(node);
733
755
 
734
- this.parseTemplateBody(node.body);
735
- this.#path.pop();
736
- this.exitScope();
756
+ this.parseTemplateBody(node.body);
757
+ this.#path.pop();
758
+ this.exitScope();
737
759
 
738
- this.next();
739
- skipWhitespace(this);
740
- this.finishNode(node, 'Component');
741
- this.awaitPos = 0;
760
+ this.next();
761
+ skipWhitespace(this);
762
+ this.finishNode(node, 'Component');
763
+ this.awaitPos = 0;
742
764
 
743
- return node;
765
+ return node;
766
+ }
767
+
768
+ parseExportDefaultDeclaration() {
769
+ // Check if this is "export default component"
770
+ if (this.value === 'component') {
771
+ return this.parseComponent({ isDefault: true });
744
772
  }
745
773
 
746
774
  return super.parseExportDefaultDeclaration();
@@ -1650,31 +1678,8 @@ function RipplePlugin(config) {
1650
1678
 
1651
1679
  if (this.value === 'component') {
1652
1680
  this.awaitPos = 0;
1653
- const node = this.startNode();
1654
- node.type = 'Component';
1655
- node.css = null;
1656
- this.next();
1657
- this.enterScope(0);
1658
- node.id = this.parseIdent();
1659
- this.declareName(node.id.name, 'var', node.id.start);
1660
- this.parseFunctionParams(node);
1661
- this.eat(tt.braceL);
1662
- node.body = [];
1663
- this.#path.push(node);
1664
-
1665
- this.parseTemplateBody(node.body);
1666
-
1667
- this.#path.pop();
1668
- this.exitScope();
1669
-
1670
- this.next();
1671
- skipWhitespace(this);
1672
- this.finishNode(node, 'Component');
1673
- this.awaitPos = 0;
1674
-
1675
- return node;
1681
+ return this.parseComponent({ requireName: true, declareName: true });
1676
1682
  }
1677
-
1678
1683
  if (this.type.label === '@') {
1679
1684
  // Try to parse as an expression statement first using tryParse
1680
1685
  // This allows us to handle Ripple @ syntax like @count++ without
@@ -356,10 +356,13 @@ const visitors = {
356
356
  }
357
357
 
358
358
  // Store component metadata in analysis
359
- context.state.analysis.component_metadata.push({
360
- id: node.id.name,
361
- async: metadata.await,
362
- });
359
+ // Only add metadata if component has a name (not anonymous)
360
+ if (node.id) {
361
+ context.state.analysis.component_metadata.push({
362
+ id: node.id.name,
363
+ async: metadata.await,
364
+ });
365
+ }
363
366
  },
364
367
 
365
368
  ForStatement(node, context) {
@@ -150,7 +150,10 @@ export function create_scopes(ast, root, parent) {
150
150
  const scope = state.scope.child();
151
151
  scopes.set(node, scope);
152
152
 
153
- scope.declare(node.id, 'normal', 'component');
153
+ // Only declare the component name if it has an id (not anonymous)
154
+ if (node.id) {
155
+ scope.declare(node.id, 'normal', 'component');
156
+ }
154
157
 
155
158
  add_params(scope, node.params);
156
159
  next({ scope });
@@ -90,4 +90,5 @@ export {
90
90
  bindInnerHTML,
91
91
  bindInnerText,
92
92
  bindTextContent,
93
+ bindNode,
93
94
  } from './internal/client/bindings.js';
@@ -1,10 +1,18 @@
1
- /** @import { Block, Tracked } from '#client' */
1
+ /** @import { Tracked } from '#client' */
2
2
 
3
3
  import { effect, render } from './blocks.js';
4
4
  import { on } from './events.js';
5
- import { active_block, get, set, tick, untrack } from './runtime.js';
5
+ import { get, set, tick, untrack } from './runtime.js';
6
6
  import { is_array, is_tracked_object } from './utils.js';
7
7
 
8
+ /**
9
+ * @param {string} name
10
+ * @returns {TypeError}
11
+ */
12
+ function not_tracked_type_error(name) {
13
+ return new TypeError(`${name} argument is not a tracked object`);
14
+ }
15
+
8
16
  /**
9
17
  * Resize observer singleton.
10
18
  * One listener per element only!
@@ -145,10 +153,9 @@ function select_option(select, value, mounting = false) {
145
153
  */
146
154
  export function bindValue(maybe_tracked) {
147
155
  if (!is_tracked_object(maybe_tracked)) {
148
- throw new TypeError('bindValue() argument is not a tracked object');
156
+ throw not_tracked_type_error('bindValue()');
149
157
  }
150
158
 
151
- var block = /** @type {Block} */ (active_block);
152
159
  var tracked = /** @type {Tracked} */ (maybe_tracked);
153
160
 
154
161
  return (node) => {
@@ -246,10 +253,9 @@ export function bindValue(maybe_tracked) {
246
253
  */
247
254
  export function bindChecked(maybe_tracked) {
248
255
  if (!is_tracked_object(maybe_tracked)) {
249
- throw new TypeError('bindChecked() argument is not a tracked object');
256
+ throw not_tracked_type_error('bindChecked()');
250
257
  }
251
258
 
252
- const block = /** @type {any} */ (active_block);
253
259
  const tracked = /** @type {Tracked} */ (maybe_tracked);
254
260
 
255
261
  return (input) => {
@@ -267,12 +273,9 @@ export function bindChecked(maybe_tracked) {
267
273
  */
268
274
  function bind_element_size(maybe_tracked, type) {
269
275
  if (!is_tracked_object(maybe_tracked)) {
270
- throw new TypeError(
271
- `bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
272
- );
276
+ throw not_tracked_type_error(`bind${type.charAt(0).toUpperCase() + type.slice(1)}()`);
273
277
  }
274
278
 
275
- var block = /** @type {any} */ (active_block);
276
279
  var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
277
280
 
278
281
  return (/** @type {HTMLElement} */ element) => {
@@ -325,12 +328,9 @@ export function bindOffsetHeight(maybe_tracked) {
325
328
  */
326
329
  function bind_element_rect(maybe_tracked, type) {
327
330
  if (!is_tracked_object(maybe_tracked)) {
328
- throw new TypeError(
329
- `bind${type.charAt(0).toUpperCase() + type.slice(1)}() argument is not a tracked object`,
330
- );
331
+ throw not_tracked_type_error(`bind${type.charAt(0).toUpperCase() + type.slice(1)}()`);
331
332
  }
332
333
 
333
- var block = /** @type {any} */ (active_block);
334
334
  var tracked = /** @type {Tracked<any>} */ (maybe_tracked);
335
335
  var observer =
336
336
  type === 'contentRect' || type === 'contentBoxSize'
@@ -388,12 +388,9 @@ export function bindDevicePixelContentBoxSize(maybe_tracked) {
388
388
  */
389
389
  export function bind_content_editable(maybe_tracked, property) {
390
390
  if (!is_tracked_object(maybe_tracked)) {
391
- throw new TypeError(
392
- `bind${property.charAt(0).toUpperCase() + property.slice(1)}() argument is not a tracked object`,
393
- );
391
+ throw not_tracked_type_error(`bind${property.charAt(0).toUpperCase() + property.slice(1)}()`);
394
392
  }
395
393
 
396
- const block = /** @type {any} */ (active_block);
397
394
  const tracked = /** @type {Tracked} */ (maybe_tracked);
398
395
 
399
396
  return (element) => {
@@ -443,3 +440,21 @@ export function bindInnerText(maybe_tracked) {
443
440
  export function bindTextContent(maybe_tracked) {
444
441
  return bind_content_editable(maybe_tracked, 'textContent');
445
442
  }
443
+
444
+ /**
445
+ * Syntactic sugar for binding a HTMLElement with {ref fn}
446
+ * @param {unknown} maybe_tracked
447
+ * @returns {(node: HTMLElement) => void}
448
+ */
449
+ export function bindNode(maybe_tracked) {
450
+ if (!is_tracked_object(maybe_tracked)) {
451
+ throw not_tracked_type_error('bindNode()');
452
+ }
453
+
454
+ const tracked = /** @type {Tracked} */ (maybe_tracked);
455
+
456
+ /** @param {HTMLElement} node */
457
+ return (node) => {
458
+ set(tracked, node);
459
+ };
460
+ }
@@ -232,4 +232,40 @@ describe('basic client > components & composition', () => {
232
232
  flushSync();
233
233
  expect(countSpan.textContent).toBe('1');
234
234
  });
235
+
236
+ it('renders components as named and anonymous properties', () => {
237
+ const UI = {
238
+ span: component Span(){
239
+ <span>{'Hello from Span'}</span>
240
+ },
241
+ button: component({ children }) {
242
+ <button>
243
+ <children />
244
+ </button>
245
+ }
246
+ };
247
+
248
+ component App(){
249
+ <div>
250
+ <h1>{'Component as Property Test'}</h1>
251
+ <UI.span />
252
+ <UI.button>
253
+ component children() {
254
+ <span>{'Click me!'}</span>
255
+ }
256
+ </UI.button>
257
+ </div>
258
+ }
259
+
260
+ render(App);
261
+
262
+ const heading = container.querySelector('h1');
263
+ const span = container.querySelector('span');
264
+ const button = container.querySelector('button');
265
+ const buttonSpan = button.querySelector('span');
266
+
267
+ expect(heading.textContent).toBe('Component as Property Test');
268
+ expect(span.textContent).toBe('Hello from Span');
269
+ expect(buttonSpan.textContent).toBe('Click me!');
270
+ });
235
271
  });
package/types/index.d.ts CHANGED
@@ -287,3 +287,5 @@ export declare function bindInnerHTML<V>(tracked: Tracked<V>): (node: HTMLElemen
287
287
  export declare function bindInnerText<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
288
288
 
289
289
  export declare function bindTextContent<V>(tracked: Tracked<V>): (node: HTMLElement) => void;
290
+
291
+ export declare function bindNode<V>(tracked: Tracked<V>): (node: HTMLElement) => void;