ripple 0.2.35 → 0.2.36

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 a TypeScript UI framework for the web",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.35",
6
+ "version": "0.2.36",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -25,6 +25,18 @@ function RipplePlugin(config) {
25
25
  class RippleParser extends Parser {
26
26
  #path = [];
27
27
 
28
+ // Helper method to get the element name from a JSX identifier or member expression
29
+ getElementName(node) {
30
+ if (!node) return null;
31
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
32
+ return node.name;
33
+ } else if (node.type === 'MemberExpression' || node.type === 'JSXMemberExpression') {
34
+ // For components like <Foo.Bar>, return "Foo.Bar"
35
+ return this.getElementName(node.object) + '.' + this.getElementName(node.property);
36
+ }
37
+ return null;
38
+ }
39
+
28
40
  parseExportDefaultDeclaration() {
29
41
  // Check if this is "export default component"
30
42
  if (this.value === 'component') {
@@ -432,11 +444,19 @@ function RipplePlugin(config) {
432
444
  this.context.pop();
433
445
  }
434
446
 
447
+ this.finishNode(element, 'Element');
435
448
  return element;
436
449
  } else {
437
450
  this.enterScope(0);
438
451
  this.parseTemplateBody(element.children);
439
452
  this.exitScope();
453
+
454
+ // Check if this element was properly closed
455
+ // If we reach here and this element is still in the path, it means it was never closed
456
+ if (this.#path[this.#path.length - 1] === element) {
457
+ const tagName = this.getElementName(element.id);
458
+ this.raise(this.start, `Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`);
459
+ }
440
460
  }
441
461
  // Ensure we escape JSX <tag></tag> context
442
462
  const tokContexts = this.acornTypeScript.tokContexts;
@@ -517,8 +537,22 @@ function RipplePlugin(config) {
517
537
  this.next();
518
538
  if (this.value === '/') {
519
539
  this.next();
520
- this.jsx_parseElementName();
540
+ const closingTag = this.jsx_parseElementName();
521
541
  this.exprAllowed = true;
542
+
543
+ // Validate that the closing tag matches the opening tag
544
+ const currentElement = this.#path[this.#path.length - 1];
545
+ if (!currentElement || currentElement.type !== 'Element') {
546
+ this.raise(this.start, 'Unexpected closing tag');
547
+ }
548
+
549
+ const openingTagName = this.getElementName(currentElement.id);
550
+ const closingTagName = this.getElementName(closingTag);
551
+
552
+ if (openingTagName !== closingTagName) {
553
+ this.raise(this.start, `Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`);
554
+ }
555
+
522
556
  this.#path.pop();
523
557
  this.next();
524
558
  return;
@@ -26,20 +26,24 @@ function visit_function(node, context) {
26
26
  if (node.params.length > 0) {
27
27
  for (let i = 0; i < node.params.length; i += 1) {
28
28
  const param = node.params[i];
29
- if (param.type === 'ObjectPattern') {
29
+
30
+ if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
30
31
  const paths = extract_paths(param);
32
+ const id = context.state.scope.generate('__arg');
33
+ const arg_id = b.id(id);
31
34
 
32
35
  for (const path of paths) {
33
36
  const name = path.node.name;
37
+
38
+ const expression = path.expression(arg_id);
34
39
  const binding = context.state.scope.get(name);
35
40
 
36
41
  if (binding !== null && is_tracked_name(name)) {
37
- const id = context.state.scope.generate('arg');
38
42
  node.params[i] = b.id(id);
39
43
  binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
40
44
 
41
45
  binding.transform = {
42
- read: (_) => b.call('$.get_property', b.id(id), b.literal(name)),
46
+ read: (_, __, visit) => visit(expression),
43
47
  };
44
48
  }
45
49
  }
@@ -55,7 +59,7 @@ function visit_function(node, context) {
55
59
  }
56
60
 
57
61
  function mark_as_tracked(path) {
58
- for (let i = 0; i < path.length; i += 1) {
62
+ for (let i = path.length - 1; i >= 0; i -= 1) {
59
63
  const node = path[i];
60
64
 
61
65
  if (node.type === 'Component') {
@@ -75,7 +75,7 @@ function build_getter(node, context) {
75
75
 
76
76
  // don't transform the declaration itself
77
77
  if (node !== binding?.node && binding?.transform?.read) {
78
- return binding.transform.read(node, context.state?.metadata?.spread);
78
+ return binding.transform.read(node, context.state?.metadata?.spread, context.visit);
79
79
  }
80
80
  }
81
81
 
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { mount, flushSync, effect } from 'ripple';
3
+ import { compile } from 'ripple/compiler';
3
4
 
4
5
  describe('basic', () => {
5
6
  let container;
@@ -833,6 +834,25 @@ describe('basic', () => {
833
834
  expect(paragraphs[1].className).toBe('nested-class');
834
835
  });
835
836
 
837
+ it('should throw error for unclosed tag', () => {
838
+ const malformedCode = `export default component Example() {
839
+ <div></span>
840
+ }`;
841
+ expect(() => {
842
+ compile(malformedCode, 'test.ripple', 'test.ripple');
843
+ }).toThrow('Expected closing tag to match opening tag');
844
+ });
845
+
846
+ it('should throw error for completely unclosed tag', () => {
847
+ const malformedCode = `export default component Example() {
848
+ <div>content
849
+ }`;
850
+
851
+ expect(() => {
852
+ compile(malformedCode, 'test.ripple', 'test.ripple');
853
+ }).toThrow('Unclosed tag');
854
+ });
855
+
836
856
  it('basic reactivity with standard arrays should work', () => {
837
857
  let logs = [];
838
858
 
@@ -897,5 +917,5 @@ describe('basic', () => {
897
917
  expect(divs[2].textContent).toBe('Number to string: 1,1');
898
918
  expect(divs[3].textContent).toBe('Even numbers: ');
899
919
  expect(logs).toEqual(['0, 0', '1, 0', 'arr includes 1', '1, 1']);
900
- })
901
- });
920
+ });
921
+ });
@@ -35,7 +35,7 @@ describe('passing reactivity between boundaries tests', () => {
35
35
 
36
36
  component App() {
37
37
  let $count = 0;
38
-
38
+
39
39
  const [ $double ] = createDouble([ $count ]);
40
40
 
41
41
  <div>{'Double: ' + $double}</div>