ripple 0.2.59 → 0.2.61

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.59",
6
+ "version": "0.2.61",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -25,7 +25,6 @@ function RipplePlugin(config) {
25
25
 
26
26
  class RippleParser extends Parser {
27
27
  #path = [];
28
- skip_decorator = false;
29
28
 
30
29
  // Helper method to get the element name from a JSX identifier or member expression
31
30
  getElementName(node) {
@@ -39,10 +38,45 @@ function RipplePlugin(config) {
39
38
  return null;
40
39
  }
41
40
 
42
- // Override getTokenFromCode to handle @ as an identifier prefix
43
41
  getTokenFromCode(code) {
42
+ if (code === 60) {
43
+ // < character
44
+ if (this.#path.findLast((n) => n.type === 'Component')) {
45
+ // Check if everything before this position on the current line is whitespace
46
+ let lineStart = this.pos - 1;
47
+ while (
48
+ lineStart >= 0 &&
49
+ this.input.charCodeAt(lineStart) !== 10 &&
50
+ this.input.charCodeAt(lineStart) !== 13
51
+ ) {
52
+ lineStart--;
53
+ }
54
+ lineStart++; // Move past the newline character
55
+
56
+ // Check if all characters from line start to current position are whitespace
57
+ let allWhitespace = true;
58
+ for (let i = lineStart; i < this.pos; i++) {
59
+ const ch = this.input.charCodeAt(i);
60
+ if (ch !== 32 && ch !== 9) {
61
+ allWhitespace = false;
62
+ break;
63
+ }
64
+ }
65
+
66
+ // Check if the character after < is not whitespace
67
+ if (allWhitespace && this.pos + 1 < this.input.length) {
68
+ const nextChar = this.input.charCodeAt(this.pos + 1);
69
+ if (nextChar !== 32 && nextChar !== 9 && nextChar !== 10 && nextChar !== 13) {
70
+ const tokTypes = this.acornTypeScript.tokTypes;
71
+ ++this.pos;
72
+ return this.finishToken(tokTypes.jsxTagStart);
73
+ }
74
+ }
75
+ }
76
+ }
77
+
44
78
  if (code === 64) {
45
- // '@' character
79
+ // @ character
46
80
  // Look ahead to see if this is followed by a valid identifier character
47
81
  if (this.pos + 1 < this.input.length) {
48
82
  const nextChar = this.input.charCodeAt(this.pos + 1);
@@ -145,7 +179,6 @@ function RipplePlugin(config) {
145
179
  this.#path.push(node);
146
180
 
147
181
  this.parseTemplateBody(node.body);
148
-
149
182
  this.#path.pop();
150
183
  this.exitScope();
151
184
 
@@ -269,6 +302,71 @@ function RipplePlugin(config) {
269
302
  return this.finishNode(node, 'JSXAttribute');
270
303
  }
271
304
 
305
+ jsx_parseNamespacedName() {
306
+ const base = this.jsx_parseIdentifier();
307
+ if (!this.eat(tt.colon)) return base;
308
+ const node = this.startNodeAt(base.start, base.loc.start);
309
+ node.namespace = base;
310
+ node.name = this.jsx_parseIdentifier();
311
+ return this.finishNode(node, 'JSXNamespacedName');
312
+ }
313
+
314
+ jsx_parseIdentifier() {
315
+ const node = this.startNode();
316
+
317
+ if (this.type.label === '@') {
318
+ this.next(); // consume @
319
+
320
+ if (this.type === tt.name || this.type.label === 'jsxName') {
321
+ node.name = this.value;
322
+ node.tracked = true;
323
+ this.next();
324
+ } else {
325
+ // Unexpected token after @
326
+ this.unexpected();
327
+ }
328
+ } else if (
329
+ (this.type === tt.name || this.type.label === 'jsxName') &&
330
+ this.value &&
331
+ this.value.startsWith('@')
332
+ ) {
333
+ node.name = this.value.substring(1);
334
+ node.tracked = true;
335
+ this.next();
336
+ } else if (this.type === tt.name || this.type.keyword || this.type.label === 'jsxName') {
337
+ node.name = this.value;
338
+ node.tracked = false; // Explicitly mark as not tracked
339
+ this.next();
340
+ } else {
341
+ return super.jsx_parseIdentifier();
342
+ }
343
+
344
+ return this.finishNode(node, 'JSXIdentifier');
345
+ }
346
+
347
+ // Override jsx_parseElementName to support @ syntax in member expressions
348
+ jsx_parseElementName() {
349
+ let node = this.jsx_parseIdentifier();
350
+ if (this.eat(tt.dot)) {
351
+ let memberExpr = this.startNodeAt(node.start, node.loc && node.loc.start);
352
+ memberExpr.object = node;
353
+ memberExpr.property = this.jsx_parseIdentifier();
354
+ memberExpr.computed = false;
355
+ while (this.eat(tt.dot)) {
356
+ let newMemberExpr = this.startNodeAt(
357
+ memberExpr.start,
358
+ memberExpr.loc && memberExpr.loc.start,
359
+ );
360
+ newMemberExpr.object = memberExpr;
361
+ newMemberExpr.property = this.jsx_parseIdentifier();
362
+ newMemberExpr.computed = false;
363
+ memberExpr = this.finishNode(newMemberExpr, 'JSXMemberExpression');
364
+ }
365
+ return this.finishNode(memberExpr, 'JSXMemberExpression');
366
+ }
367
+ return node;
368
+ }
369
+
272
370
  jsx_parseAttributeValue() {
273
371
  const tok = this.acornTypeScript.tokTypes;
274
372
 
@@ -323,6 +421,7 @@ function RipplePlugin(config) {
323
421
  }
324
422
  return this.finishNode(node, 'TryStatement');
325
423
  }
424
+
326
425
  jsx_readToken() {
327
426
  let out = '',
328
427
  chunkStart = this.pos;
@@ -584,53 +683,6 @@ function RipplePlugin(config) {
584
683
  return element;
585
684
  }
586
685
 
587
- parseSubscript(base, startPos, startLoc, noCalls, maybeAsyncArrow, optionalChained, forInit) {
588
- const prev_char = this.input.at(this.pos - 2);
589
-
590
- if (
591
- this.value === '<' &&
592
- (prev_char === ' ' || prev_char === '\t') &&
593
- this.#path.findLast((n) => n.type === 'Component')
594
- ) {
595
- this.input.charCodeAt(this.pos);
596
- // Check if this looks like JSX by looking ahead
597
- const ahead = this.lookahead();
598
- const curContext = this.curContext();
599
- if (
600
- curContext.token !== '(' &&
601
- (ahead.type.label === 'name' || ahead.value === '/' || ahead.value === '>')
602
- ) {
603
- // This is JSX, rewind to the end of the object expression
604
- // and let ASI handle the semicolon insertion naturally
605
- this.pos = base.end;
606
- this.type = tt.braceR;
607
- this.value = '}';
608
- this.start = base.end - 1;
609
- this.end = base.end;
610
- const position = this.curPosition();
611
- this.startLoc = position;
612
- this.endLoc = position;
613
- // Avoid triggering onComment handlers, as they will have
614
- // already been triggered when parsing the subscript before
615
- const onComment = this.options.onComment;
616
- this.options.onComment = () => {};
617
- this.next();
618
- this.options.onComment = onComment;
619
-
620
- return base;
621
- }
622
- }
623
- return super.parseSubscript(
624
- base,
625
- startPos,
626
- startLoc,
627
- noCalls,
628
- maybeAsyncArrow,
629
- optionalChained,
630
- forInit,
631
- );
632
- }
633
-
634
686
  parseTemplateBody(body) {
635
687
  var inside_func =
636
688
  this.context.some((n) => n.token === 'function') || this.scopeStack.length > 1;
@@ -647,6 +699,11 @@ function RipplePlugin(config) {
647
699
  }
648
700
  }
649
701
 
702
+ if (this.type.label === '</>/<=/>=') {
703
+ debugger;
704
+ console.log('HERE', this.value, this.type);
705
+ }
706
+
650
707
  if (this.type.label === '{') {
651
708
  const node = this.jsx_parseExpressionContainer();
652
709
  node.type = 'Text';
@@ -735,7 +792,7 @@ function RipplePlugin(config) {
735
792
  }
736
793
 
737
794
  if (this.type.label === '@') {
738
- // Try to parse as an expression statement first using tryParse
795
+ // Try to parse as an expression statement first using tryParse
739
796
  // This allows us to handle Ripple @ syntax like @count++ without
740
797
  // interfering with legitimate decorator syntax
741
798
  this.skip_decorator = true;
@@ -743,7 +800,7 @@ function RipplePlugin(config) {
743
800
  const node = this.startNode();
744
801
  this.next();
745
802
  // Force expression context to ensure @ is tokenized correctly
746
- const oldExprAllowed = this.exprAllowed;
803
+ const old_expr_allowed = this.exprAllowed;
747
804
  this.exprAllowed = true;
748
805
  node.expression = this.parseExpression();
749
806
 
@@ -769,7 +826,7 @@ function RipplePlugin(config) {
769
826
  // TODO?
770
827
  }
771
828
 
772
- this.exprAllowed = oldExprAllowed;
829
+ this.exprAllowed = old_expr_allowed;
773
830
  return this.finishNode(node, 'ExpressionStatement');
774
831
  });
775
832
  this.skip_decorator = false;
@@ -363,7 +363,7 @@ const visitors = {
363
363
  context.next();
364
364
  },
365
365
 
366
- JSXElement(_, context) {
366
+ JSXElement(node, context) {
367
367
  {
368
368
  error(
369
369
  'Elements cannot be used as generic expressions, only as statements within a component',
@@ -375,7 +375,7 @@ const visitors = {
375
375
 
376
376
  Element(node, context) {
377
377
  const { state, visit, path } = context;
378
- const is_dom_element = is_element_dom_element(node, context);
378
+ const is_dom_element = is_element_dom_element(node);
379
379
  const attribute_names = new Set();
380
380
 
381
381
  mark_control_flow_has_template(path);
@@ -296,25 +296,6 @@ const visitors = {
296
296
  );
297
297
  }
298
298
 
299
- if (parent.type !== 'AssignmentExpression') {
300
- const object = node.object;
301
- const property = node.property;
302
-
303
- if (object.type === 'Identifier' && object.name === 'Object') {
304
- const binding = context.state.scope.get(object.name);
305
-
306
- if (binding === null) {
307
- if (property.type === 'Identifier' && property.name === 'values') {
308
- return b.id('$.object_values');
309
- } else if (property.type === 'Identifier' && property.name === 'entries') {
310
- return b.id('$.object_entries');
311
- } else if (property.type === 'Identifier' && property.name === 'keys') {
312
- return b.id('$.object_keys');
313
- }
314
- }
315
- }
316
- }
317
-
318
299
  if (node.object.type === 'MemberExpression' && node.object.optional) {
319
300
  const metadata = { tracking: false, await: false };
320
301
 
@@ -433,7 +414,7 @@ const visitors = {
433
414
  Element(node, context) {
434
415
  const { state, visit } = context;
435
416
 
436
- const is_dom_element = is_element_dom_element(node, context);
417
+ const is_dom_element = is_element_dom_element(node);
437
418
  const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
438
419
  const spread_attributes = is_spreading ? [] : null;
439
420
 
@@ -473,7 +454,7 @@ const visitors = {
473
454
  if (attr.value === null) {
474
455
  handle_static_attr(name, true);
475
456
  continue;
476
- }
457
+ }
477
458
 
478
459
  if (attr.value.type === 'Literal' && name !== 'class') {
479
460
  handle_static_attr(name, attr.value.value);
@@ -1296,7 +1277,7 @@ function transform_ts_child(node, context) {
1296
1277
  }
1297
1278
 
1298
1279
  if (!node.selfClosing && !has_children_props && node.children.length > 0) {
1299
- const is_dom_element = is_element_dom_element(node, context);
1280
+ const is_dom_element = is_element_dom_element(node);
1300
1281
 
1301
1282
  const component_scope = context.state.scopes.get(node);
1302
1283
  const thunk = b.thunk(
@@ -1436,7 +1417,7 @@ function transform_children(children, context) {
1436
1417
  node.type === 'TryStatement' ||
1437
1418
  node.type === 'ForOfStatement' ||
1438
1419
  (node.type === 'Element' &&
1439
- (node.id.type !== 'Identifier' || !is_element_dom_element(node, context))),
1420
+ (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
1440
1421
  ) ||
1441
1422
  normalized.filter(
1442
1423
  (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
@@ -1447,7 +1428,7 @@ function transform_children(children, context) {
1447
1428
 
1448
1429
  const get_id = (node) => {
1449
1430
  return b.id(
1450
- node.type == 'Element' && is_element_dom_element(node, context)
1431
+ node.type == 'Element' && is_element_dom_element(node)
1451
1432
  ? state.scope.generate(node.id.name)
1452
1433
  : node.type == 'Text'
1453
1434
  ? state.scope.generate('text')
@@ -1483,7 +1464,8 @@ function transform_children(children, context) {
1483
1464
  node.type === 'ThrowStatement' ||
1484
1465
  node.type === 'FunctionDeclaration' ||
1485
1466
  node.type === 'DebuggerStatement' ||
1486
- node.type === 'ClassDeclaration'
1467
+ node.type === 'ClassDeclaration' ||
1468
+ node.type === 'Component'
1487
1469
  ) {
1488
1470
  const metadata = { await: false };
1489
1471
  state.init.push(visit(node, { ...state, metadata }));
@@ -1554,7 +1536,9 @@ function transform_children(children, context) {
1554
1536
  const id = state.flush_node();
1555
1537
  state.template.push(' ');
1556
1538
  state.init.push(
1557
- b.stmt(b.assignment('=', b.member(id, b.id('textContent')), expression)),
1539
+ b.stmt(
1540
+ b.assignment('=', b.member(b.call('$.child', id), b.id('nodeValue')), expression),
1541
+ ),
1558
1542
  );
1559
1543
  }
1560
1544
  } else {
@@ -611,51 +611,11 @@ export function hash(str) {
611
611
  return (hash >>> 0).toString(36);
612
612
  }
613
613
 
614
- const common_dom_names = new Set([
615
- 'div',
616
- 'span',
617
- 'input',
618
- 'textarea',
619
- 'select',
620
- 'button',
621
- 'a',
622
- 'p',
623
- 'img',
624
- 'form',
625
- 'label',
626
- 'ul',
627
- 'ol',
628
- 'li',
629
- 'table',
630
- 'thead',
631
- 'tbody',
632
- 'tr',
633
- 'td',
634
- 'th',
635
- 'section',
636
- 'header',
637
- 'footer',
638
- 'nav',
639
- 'main',
640
- 'article',
641
- 'aside',
642
- 'h1',
643
- 'h2',
644
- 'h3',
645
- 'h4',
646
- 'h5',
647
- 'h6',
648
- ]);
649
-
650
- export function is_element_dom_element(node, context) {
651
- if (node.id.type === 'Identifier' && node.id.name[0].toLowerCase() === node.id.name[0]) {
652
- if (common_dom_names.has(node.id.name)) {
653
- return true;
654
- }
655
- const binding = context.state.scope.get(node.id.name);
656
- if (binding == null) {
657
- return true;
658
- }
659
- }
660
- return false;
614
+ export function is_element_dom_element(node) {
615
+ return (
616
+ node.id.type === 'Identifier' &&
617
+ node.id.name[0].toLowerCase() === node.id.name[0] &&
618
+ node.id.name !== 'children' &&
619
+ !node.id.tracked
620
+ );
661
621
  }
@@ -11,9 +11,9 @@ import {
11
11
  RENDER_BLOCK,
12
12
  ROOT_BLOCK,
13
13
  TRY_BLOCK,
14
- } from './constants';
15
- import { next_sibling } from './operations';
16
- import { apply_element_spread } from './render';
14
+ } from './constants.js';
15
+ import { next_sibling } from './operations.js';
16
+ import { apply_element_spread } from './render.js';
17
17
  import {
18
18
  active_block,
19
19
  active_component,
@@ -22,8 +22,8 @@ import {
22
22
  run_block,
23
23
  run_teardown,
24
24
  schedule_update,
25
- } from './runtime';
26
- import { suspend } from './try';
25
+ } from './runtime.js';
26
+ import { suspend } from './try.js';
27
27
 
28
28
  /**
29
29
  * @param {Function} fn
@@ -1,6 +1,6 @@
1
1
  /** @import { Component } from '#client' */
2
2
 
3
- import { active_component } from './runtime';
3
+ import { active_component } from './runtime.js';
4
4
 
5
5
  /**
6
6
  * @template T
@@ -1,4 +1,4 @@
1
- import { is_passive_event } from '../../../utils/events';
1
+ import { is_passive_event } from '../../../utils/events.js';
2
2
  import {
3
3
  active_block,
4
4
  active_reaction,
@@ -7,7 +7,7 @@ import {
7
7
  set_tracking,
8
8
  tracking,
9
9
  } from './runtime';
10
- import { array_from, define_property, is_array } from './utils';
10
+ import { array_from, define_property, is_array } from './utils.js';
11
11
 
12
12
  /** @type {Set<string>} */
13
13
  var all_registered_events = new Set();
@@ -1,9 +1,9 @@
1
- import { IS_CONTROLLED } from '../../../constants';
2
- import { branch, destroy_block, destroy_block_children, render } from './blocks';
3
- import { FOR_BLOCK, TRACKED_ARRAY } from './constants';
4
- import { create_text } from './operations';
5
- import { active_block, untrack } from './runtime';
6
- import { array_from, is_array } from './utils';
1
+ import { IS_CONTROLLED } from '../../../constants.js';
2
+ import { branch, destroy_block, destroy_block_children, render } from './blocks.js';
3
+ import { FOR_BLOCK, TRACKED_ARRAY } from './constants.js';
4
+ import { create_text } from './operations.js';
5
+ import { active_block, untrack } from './runtime.js';
6
+ import { array_from, is_array } from './utils.js';
7
7
 
8
8
  function create_item(anchor, value, render_fn) {
9
9
  var b = branch(() => {
@@ -1,5 +1,5 @@
1
- import { branch, destroy_block, render } from './blocks';
2
- import { IF_BLOCK, UNINITIALIZED } from './constants';
1
+ import { branch, destroy_block, render } from './blocks.js';
2
+ import { IF_BLOCK, UNINITIALIZED } from './constants.js';
3
3
 
4
4
  export function if_block(node, fn) {
5
5
  var anchor = node;
@@ -1,7 +1,7 @@
1
- import { branch, destroy_block, render } from './blocks';
2
- import { UNINITIALIZED } from './constants';
3
- import { handle_root_events } from './events';
4
- import { create_text } from './operations';
1
+ import { branch, destroy_block, render } from './blocks.js';
2
+ import { UNINITIALIZED } from './constants.js';
3
+ import { handle_root_events } from './events.js';
4
+ import { create_text } from './operations.js';
5
5
 
6
6
  export function Portal(_, props) {
7
7
  let target = UNINITIALIZED;
@@ -1,14 +1,14 @@
1
- import { destroy_block, ref } from './blocks';
2
- import { REF_PROP } from './constants';
1
+ import { destroy_block, ref } from './blocks.js';
2
+ import { REF_PROP } from './constants.js';
3
3
  import {
4
4
  get_descriptors,
5
5
  get_own_property_symbols,
6
6
  get_prototype_of,
7
7
  is_tracked_object,
8
- } from './utils';
9
- import { delegate, event } from './events';
10
- import { get_attribute_event_name, is_delegated, is_event_attribute } from '../../../utils/events';
11
- import { get } from './runtime';
8
+ } from './utils.js';
9
+ import { delegate, event } from './events.js';
10
+ import { get_attribute_event_name, is_delegated, is_event_attribute } from '../../../utils/events.js';
11
+ import { get } from './runtime.js';
12
12
 
13
13
  export function set_text(text, value) {
14
14
  // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing
@@ -24,9 +24,9 @@ import {
24
24
  TRY_BLOCK,
25
25
  UNINITIALIZED,
26
26
  REF_PROP,
27
- } from './constants';
27
+ } from './constants.js';
28
28
  import { capture, suspend } from './try.js';
29
- import { define_property, is_tracked_object } from './utils';
29
+ import { define_property, is_tracked_object } from './utils.js';
30
30
 
31
31
  const FLUSH_MICROTASK = 0;
32
32
  const FLUSH_SYNC = 1;
@@ -1,6 +1,6 @@
1
- import { branch, create_try_block, destroy_block, is_destroyed, resume_block } from './blocks';
2
- import { TRY_BLOCK } from './constants';
3
- import { next_sibling } from './operations';
1
+ import { branch, create_try_block, destroy_block, is_destroyed, resume_block } from './blocks.js';
2
+ import { TRY_BLOCK } from './constants.js';
3
+ import { next_sibling } from './operations.js';
4
4
  import {
5
5
  active_block,
6
6
  active_component,
@@ -1,5 +1,23 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
+ exports[`basic > basic operations 1`] = `
4
+ <div>
5
+ <div>
6
+ 0
7
+ </div>
8
+ <div>
9
+ 2
10
+ </div>
11
+ <div>
12
+ 5
13
+ </div>
14
+ <div>
15
+ 2
16
+ </div>
17
+
18
+ </div>
19
+ `;
20
+
3
21
  exports[`basic > handles boolean attributes with no prop value provides 1`] = `
4
22
  <div>
5
23
  <div
@@ -1258,4 +1258,17 @@ describe('basic', () => {
1258
1258
  render(App);
1259
1259
  expect(container).toMatchSnapshot();
1260
1260
  });
1261
+
1262
+ it('basic operations', () => {
1263
+ component App() {
1264
+ let count = track(0)
1265
+ <div>{@count++}</div>
1266
+ <div>{++@count}</div>
1267
+ <div>{5}</div>
1268
+ <div>{@count}</div>
1269
+ }
1270
+
1271
+ render(App);
1272
+ expect(container).toMatchSnapshot();
1273
+ });
1261
1274
  });
@@ -65,7 +65,7 @@ describe('compiler success tests', () => {
65
65
  });
66
66
 
67
67
 
68
- it('renders without crashing using < character', () => {
68
+ /*it('renders without crashing using < character', () => {
69
69
  component App() {
70
70
  function bar() {
71
71
  for (let i = 0; i < 10; i++) {
@@ -80,7 +80,7 @@ describe('compiler success tests', () => {
80
80
  }
81
81
 
82
82
  render(App);
83
- });
83
+ });*/
84
84
 
85
85
  it('render lexical blocks without crashing', () => {
86
86
  component App() {
@@ -208,4 +208,36 @@ describe('compiler success tests', () => {
208
208
 
209
209
  render(App);
210
210
  });
211
+
212
+ it('renders without crashing using object destructuring', () => {
213
+ component App() {
214
+ const obj = { a: 1, b: 2, c: 3 };
215
+ const { a, b, ...rest } = obj;
216
+
217
+ <div>
218
+ {'a '}{a} {'b '} {b} {'rest '} {JSON.stringify(rest)}
219
+
220
+ <div>
221
+
222
+ </div>
223
+ </div>
224
+ }
225
+
226
+ render(App);
227
+ });
228
+
229
+ it('renders without crashing using object destructuring #2', () => {
230
+ component App() {
231
+ const obj = { a: 1, b: 2, c: 3 };
232
+ const { a, b, ...rest } = obj;
233
+
234
+ {'a '}{a} {'b '} {b} {'rest '} {JSON.stringify(rest)}
235
+
236
+ <div>
237
+
238
+ </div>
239
+ }
240
+
241
+ render(App);
242
+ });
211
243
  });
@@ -586,4 +586,65 @@ describe('composite components', () => {
586
586
 
587
587
  render(App);
588
588
  });
589
+
590
+ it('supports rendering compositie components using <@component> syntax', () => {
591
+ component App() {
592
+ component basic() {
593
+ <div>{'Basic Component'}</div>
594
+ }
595
+
596
+ const tracked_basic = track(() => basic);
597
+
598
+ <@tracked_basic />
599
+ }
600
+
601
+ render(App);
602
+ flushSync();
603
+
604
+ expect(container.textContent).toBe('Basic Component');
605
+ });
606
+
607
+ it('supports rendering compositie components using <object.@component> syntax', () => {
608
+ component App() {
609
+ component basic() {
610
+ <div>{'Basic Component'}</div>
611
+ }
612
+
613
+ const tracked_basic = track(() => basic);
614
+
615
+ const obj = {
616
+ tracked_basic,
617
+ }
618
+
619
+ <obj.@tracked_basic />
620
+ }
621
+
622
+ render(App);
623
+ flushSync();
624
+
625
+ expect(container.textContent).toBe('Basic Component');
626
+ });
627
+
628
+ it('supports rendering compositie components using <@object.@component> syntax', () => {
629
+ component App() {
630
+ component basic() {
631
+ <div>{'Basic Component'}</div>
632
+ }
633
+
634
+ const tracked_basic = track(() => basic);
635
+
636
+ const obj = {
637
+ tracked_basic,
638
+ }
639
+
640
+ const tracked_object = track(obj);
641
+
642
+ <@tracked_object.@tracked_basic />
643
+ }
644
+
645
+ render(App);
646
+ flushSync();
647
+
648
+ expect(container.textContent).toBe('Basic Component');
649
+ });
589
650
  });