ripple 0.2.34 → 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 +19 -4
- package/src/compiler/phases/3-transform/index.js +11 -4
- package/src/compiler/utils.js +27 -0
- 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;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
is_ripple_import,
|
|
9
9
|
is_tracked_computed_property,
|
|
10
10
|
is_tracked_name,
|
|
11
|
+
is_void_element,
|
|
11
12
|
} from '../../utils.js';
|
|
12
13
|
import { extract_paths } from '../../../utils/ast.js';
|
|
13
14
|
import is_reference from 'is-reference';
|
|
@@ -25,20 +26,24 @@ function visit_function(node, context) {
|
|
|
25
26
|
if (node.params.length > 0) {
|
|
26
27
|
for (let i = 0; i < node.params.length; i += 1) {
|
|
27
28
|
const param = node.params[i];
|
|
28
|
-
|
|
29
|
+
|
|
30
|
+
if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
|
|
29
31
|
const paths = extract_paths(param);
|
|
32
|
+
const id = context.state.scope.generate('__arg');
|
|
33
|
+
const arg_id = b.id(id);
|
|
30
34
|
|
|
31
35
|
for (const path of paths) {
|
|
32
36
|
const name = path.node.name;
|
|
37
|
+
|
|
38
|
+
const expression = path.expression(arg_id);
|
|
33
39
|
const binding = context.state.scope.get(name);
|
|
34
40
|
|
|
35
41
|
if (binding !== null && is_tracked_name(name)) {
|
|
36
|
-
const id = context.state.scope.generate('arg');
|
|
37
42
|
node.params[i] = b.id(id);
|
|
38
43
|
binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
|
|
39
44
|
|
|
40
45
|
binding.transform = {
|
|
41
|
-
read: (_
|
|
46
|
+
read: (_, __, visit) => visit(expression),
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
}
|
|
@@ -54,7 +59,7 @@ function visit_function(node, context) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
function mark_as_tracked(path) {
|
|
57
|
-
for (let i =
|
|
62
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
58
63
|
const node = path[i];
|
|
59
64
|
|
|
60
65
|
if (node.type === 'Component') {
|
|
@@ -432,6 +437,8 @@ const visitors = {
|
|
|
432
437
|
const attribute_names = new Set();
|
|
433
438
|
|
|
434
439
|
if (is_dom_element) {
|
|
440
|
+
const is_void = is_void_element(node.id.name);
|
|
441
|
+
|
|
435
442
|
if (state.elements) {
|
|
436
443
|
state.elements.push(node);
|
|
437
444
|
}
|
|
@@ -468,6 +475,14 @@ const visitors = {
|
|
|
468
475
|
);
|
|
469
476
|
}
|
|
470
477
|
}
|
|
478
|
+
|
|
479
|
+
if (is_void && node.children.length > 0) {
|
|
480
|
+
error(
|
|
481
|
+
`The <${node.id.name}> element is a void element and cannot have children`,
|
|
482
|
+
state.analysis.module.filename,
|
|
483
|
+
node,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
471
486
|
} else {
|
|
472
487
|
for (const attr of node.attributes) {
|
|
473
488
|
if (attr.type === 'Attribute') {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
is_inside_call_expression,
|
|
22
22
|
is_tracked_computed_property,
|
|
23
23
|
is_value_static,
|
|
24
|
+
is_void_element,
|
|
24
25
|
} from '../../utils.js';
|
|
25
26
|
import is_reference from 'is-reference';
|
|
26
27
|
import { extract_paths, object } from '../../../utils/ast.js';
|
|
@@ -74,7 +75,7 @@ function build_getter(node, context) {
|
|
|
74
75
|
|
|
75
76
|
// don't transform the declaration itself
|
|
76
77
|
if (node !== binding?.node && binding?.transform?.read) {
|
|
77
|
-
return binding.transform.read(node, context.state?.metadata?.spread);
|
|
78
|
+
return binding.transform.read(node, context.state?.metadata?.spread, context.visit);
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -438,6 +439,7 @@ const visitors = {
|
|
|
438
439
|
if (is_dom_element) {
|
|
439
440
|
let class_attribute = null;
|
|
440
441
|
const local_updates = [];
|
|
442
|
+
const is_void = is_void_element(node.id.name);
|
|
441
443
|
|
|
442
444
|
state.template.push(`<${node.id.name}`);
|
|
443
445
|
|
|
@@ -629,7 +631,14 @@ const visitors = {
|
|
|
629
631
|
const init = [];
|
|
630
632
|
const update = [];
|
|
631
633
|
|
|
632
|
-
|
|
634
|
+
if (!is_void) {
|
|
635
|
+
transform_children(node.children, {
|
|
636
|
+
visit,
|
|
637
|
+
state: { ...state, init, update },
|
|
638
|
+
root: false,
|
|
639
|
+
});
|
|
640
|
+
state.template.push(`</${node.id.name}>`);
|
|
641
|
+
}
|
|
633
642
|
|
|
634
643
|
update.push(...local_updates);
|
|
635
644
|
|
|
@@ -640,8 +649,6 @@ const visitors = {
|
|
|
640
649
|
if (update.length > 0) {
|
|
641
650
|
state.init.push(b.stmt(b.call('$.render', b.thunk(b.block(update)))));
|
|
642
651
|
}
|
|
643
|
-
|
|
644
|
-
state.template.push(`</${node.id.name}>`);
|
|
645
652
|
} else {
|
|
646
653
|
const id = state.flush_node();
|
|
647
654
|
|
package/src/compiler/utils.js
CHANGED
|
@@ -3,6 +3,33 @@ import * as b from '../utils/builders.js';
|
|
|
3
3
|
|
|
4
4
|
const regex_return_characters = /\r/g;
|
|
5
5
|
|
|
6
|
+
const VOID_ELEMENT_NAMES = [
|
|
7
|
+
'area',
|
|
8
|
+
'base',
|
|
9
|
+
'br',
|
|
10
|
+
'col',
|
|
11
|
+
'command',
|
|
12
|
+
'embed',
|
|
13
|
+
'hr',
|
|
14
|
+
'img',
|
|
15
|
+
'input',
|
|
16
|
+
'keygen',
|
|
17
|
+
'link',
|
|
18
|
+
'meta',
|
|
19
|
+
'param',
|
|
20
|
+
'source',
|
|
21
|
+
'track',
|
|
22
|
+
'wbr'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns `true` if `name` is of a void element
|
|
27
|
+
* @param {string} name
|
|
28
|
+
*/
|
|
29
|
+
export function is_void_element(name) {
|
|
30
|
+
return VOID_ELEMENT_NAMES.includes(name) || name.toLowerCase() === '!doctype';
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
const RESERVED_WORDS = [
|
|
7
34
|
'arguments',
|
|
8
35
|
'await',
|
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
|