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 +1 -1
- package/src/compiler/phases/1-parse/index.js +35 -1
- package/src/compiler/phases/2-analyze/index.js +8 -4
- package/src/compiler/phases/3-transform/index.js +1 -1
- package/tests/basic.test.ripple +22 -2
- package/tests/{boundaries.ripple → boundaries.test.ripple} +1 -1
- /package/tests/{decorators.ripple → decorators.test.ripple} +0 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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: (_
|
|
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 =
|
|
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
|
|
package/tests/basic.test.ripple
CHANGED
|
@@ -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
|
+
});
|
|
File without changes
|