ripple 0.2.35 → 0.2.37
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 +53 -5
- package/src/compiler/phases/3-transform/index.js +64 -25
- package/src/runtime/internal/client/index.js +1 -1
- package/tests/basic.test.ripple +22 -2
- package/tests/{boundaries.ripple → boundaries.test.ripple} +1 -1
- package/types/index.d.ts +24 -0
- /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;
|
|
@@ -15,6 +15,29 @@ import is_reference from 'is-reference';
|
|
|
15
15
|
import { prune_css } from './prune.js';
|
|
16
16
|
import { error } from '../../errors.js';
|
|
17
17
|
|
|
18
|
+
function mark_for_loop_has_template(path) {
|
|
19
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
20
|
+
const node = path[i];
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
node.type === 'Component' ||
|
|
24
|
+
node.type === 'FunctionExpression' ||
|
|
25
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
26
|
+
node.type === 'FunctionDeclaration'
|
|
27
|
+
) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
node.type === 'ForStatement' ||
|
|
32
|
+
node.type === 'ForInStatement' ||
|
|
33
|
+
node.type === 'ForOfStatement'
|
|
34
|
+
) {
|
|
35
|
+
node.metadata.has_template = true;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
function visit_function(node, context) {
|
|
19
42
|
node.metadata = {
|
|
20
43
|
hoisted: false,
|
|
@@ -26,20 +49,24 @@ function visit_function(node, context) {
|
|
|
26
49
|
if (node.params.length > 0) {
|
|
27
50
|
for (let i = 0; i < node.params.length; i += 1) {
|
|
28
51
|
const param = node.params[i];
|
|
29
|
-
|
|
52
|
+
|
|
53
|
+
if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
|
|
30
54
|
const paths = extract_paths(param);
|
|
55
|
+
const id = context.state.scope.generate('__arg');
|
|
56
|
+
const arg_id = b.id(id);
|
|
31
57
|
|
|
32
58
|
for (const path of paths) {
|
|
33
59
|
const name = path.node.name;
|
|
60
|
+
|
|
61
|
+
const expression = path.expression(arg_id);
|
|
34
62
|
const binding = context.state.scope.get(name);
|
|
35
63
|
|
|
36
64
|
if (binding !== null && is_tracked_name(name)) {
|
|
37
|
-
const id = context.state.scope.generate('arg');
|
|
38
65
|
node.params[i] = b.id(id);
|
|
39
66
|
binding.kind = path.has_default_value ? 'prop_fallback' : 'prop';
|
|
40
67
|
|
|
41
68
|
binding.transform = {
|
|
42
|
-
read: (_
|
|
69
|
+
read: (_, __, visit) => visit(expression),
|
|
43
70
|
};
|
|
44
71
|
}
|
|
45
72
|
}
|
|
@@ -55,7 +82,7 @@ function visit_function(node, context) {
|
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
function mark_as_tracked(path) {
|
|
58
|
-
for (let i =
|
|
85
|
+
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
59
86
|
const node = path[i];
|
|
60
87
|
|
|
61
88
|
if (node.type === 'Component') {
|
|
@@ -403,6 +430,20 @@ const visitors = {
|
|
|
403
430
|
context.next();
|
|
404
431
|
},
|
|
405
432
|
|
|
433
|
+
ForOfStatement(node, context) {
|
|
434
|
+
node.metadata = {
|
|
435
|
+
has_template: false,
|
|
436
|
+
};
|
|
437
|
+
context.next();
|
|
438
|
+
if (!node.metadata.has_template) {
|
|
439
|
+
error(
|
|
440
|
+
'For...of loops must contain a template in their body. Move the for loop into an effect if it does not render anything.',
|
|
441
|
+
context.state.analysis.module.filename,
|
|
442
|
+
node,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
|
|
406
447
|
ForInStatement(node, context) {
|
|
407
448
|
if (is_inside_component(context)) {
|
|
408
449
|
error(
|
|
@@ -425,13 +466,15 @@ const visitors = {
|
|
|
425
466
|
}
|
|
426
467
|
},
|
|
427
468
|
|
|
428
|
-
Element(node, { state, visit }) {
|
|
469
|
+
Element(node, { state, visit, path }) {
|
|
429
470
|
const is_dom_element =
|
|
430
471
|
node.id.type === 'Identifier' &&
|
|
431
472
|
node.id.name[0].toLowerCase() === node.id.name[0] &&
|
|
432
473
|
node.id.name[0] !== '$';
|
|
433
474
|
const attribute_names = new Set();
|
|
434
475
|
|
|
476
|
+
mark_for_loop_has_template(path);
|
|
477
|
+
|
|
435
478
|
if (is_dom_element) {
|
|
436
479
|
const is_void = is_void_element(node.id.name);
|
|
437
480
|
|
|
@@ -563,6 +606,11 @@ const visitors = {
|
|
|
563
606
|
};
|
|
564
607
|
},
|
|
565
608
|
|
|
609
|
+
Text(node, context) {
|
|
610
|
+
mark_for_loop_has_template(context.path);
|
|
611
|
+
context.next();
|
|
612
|
+
},
|
|
613
|
+
|
|
566
614
|
AwaitExpression(node, context) {
|
|
567
615
|
if (is_inside_component(context)) {
|
|
568
616
|
if (context.state.metadata?.await === false) {
|
|
@@ -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
|
|
|
@@ -328,34 +328,52 @@ const visitors = {
|
|
|
328
328
|
delete declarator.id.typeAnnotation;
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
if (binding !== null && binding.kind === 'tracked'
|
|
331
|
+
if (binding !== null && binding.kind === 'tracked') {
|
|
332
332
|
let expression;
|
|
333
333
|
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
334
|
+
if (context.state.to_ts) {
|
|
335
|
+
// TypeScript mode: lighter transformation
|
|
336
|
+
if (metadata.tracking && !metadata.await) {
|
|
337
|
+
expression = b.call(
|
|
338
|
+
'$.computed',
|
|
339
|
+
b.thunk(context.visit(declarator.init)),
|
|
340
|
+
b.id('__block'),
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
expression = b.call(
|
|
344
|
+
'$.tracked',
|
|
345
|
+
declarator.init === null ? undefined : context.visit(declarator.init),
|
|
346
|
+
b.id('__block'),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// Runtime mode: full transformation
|
|
351
|
+
if (metadata.tracking && metadata.await) {
|
|
352
|
+
expression = b.call(
|
|
353
|
+
b.await(
|
|
339
354
|
b.call(
|
|
340
|
-
'$.
|
|
341
|
-
b.
|
|
342
|
-
|
|
355
|
+
'$.resume_context',
|
|
356
|
+
b.call(
|
|
357
|
+
'$.async_computed',
|
|
358
|
+
b.thunk(context.visit(declarator.init), true),
|
|
359
|
+
b.id('__block'),
|
|
360
|
+
),
|
|
343
361
|
),
|
|
344
362
|
),
|
|
345
|
-
)
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
363
|
+
);
|
|
364
|
+
} else if (metadata.tracking && !metadata.await) {
|
|
365
|
+
expression = b.call(
|
|
366
|
+
'$.computed',
|
|
367
|
+
b.thunk(context.visit(declarator.init)),
|
|
368
|
+
b.id('__block'),
|
|
369
|
+
);
|
|
370
|
+
} else {
|
|
371
|
+
expression = b.call(
|
|
372
|
+
'$.tracked',
|
|
373
|
+
declarator.init === null ? undefined : context.visit(declarator.init),
|
|
374
|
+
b.id('__block'),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
359
377
|
}
|
|
360
378
|
|
|
361
379
|
declarations.push(b.declarator(declarator.id, expression));
|
|
@@ -372,11 +390,32 @@ const visitors = {
|
|
|
372
390
|
delete declarator.id.typeAnnotation;
|
|
373
391
|
}
|
|
374
392
|
|
|
375
|
-
if (!has_tracked
|
|
393
|
+
if (!has_tracked) {
|
|
376
394
|
declarations.push(context.visit(declarator));
|
|
377
395
|
continue;
|
|
378
396
|
}
|
|
379
397
|
|
|
398
|
+
// For TypeScript mode, we still need to transform tracked variables
|
|
399
|
+
// but use a lighter approach that maintains type information
|
|
400
|
+
if (context.state.to_ts) {
|
|
401
|
+
const transformed = declarator.transformed || declarator.id;
|
|
402
|
+
let expression;
|
|
403
|
+
|
|
404
|
+
if (metadata.tracking && !metadata.await) {
|
|
405
|
+
expression = b.call(
|
|
406
|
+
'$.computed',
|
|
407
|
+
b.thunk(context.visit(declarator.init)),
|
|
408
|
+
b.id('__block'),
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
// Simple tracked variable - always use $.tracked for $ prefixed variables
|
|
412
|
+
expression = b.call('$.tracked', context.visit(declarator.init), b.id('__block'));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
declarations.push(b.declarator(transformed, expression));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
380
419
|
const transformed = declarator.transformed;
|
|
381
420
|
let expression;
|
|
382
421
|
|
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
|
+
});
|
package/types/index.d.ts
CHANGED
|
@@ -55,3 +55,27 @@ export class RippleMap<K, V> extends Map<K, V> {
|
|
|
55
55
|
get $size(): number;
|
|
56
56
|
toJSON(): [K, V][];
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
// Compiler-injected runtime symbols (for Ripple component development)
|
|
60
|
+
declare global {
|
|
61
|
+
/**
|
|
62
|
+
* Runtime block context injected by the Ripple compiler.
|
|
63
|
+
* This is automatically available in component scopes and passed to runtime functions.
|
|
64
|
+
*/
|
|
65
|
+
var __block: any;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Ripple runtime namespace - injected by the compiler
|
|
69
|
+
* These functions are available in compiled Ripple components for TypeScript analysis
|
|
70
|
+
*/
|
|
71
|
+
var $: {
|
|
72
|
+
tracked<T>(value: T, block?: any): T;
|
|
73
|
+
tracked_object<T extends Record<string, any>>(obj: T, props: string[], block?: any): T;
|
|
74
|
+
computed<T>(fn: () => T, block?: any): T;
|
|
75
|
+
scope(): any;
|
|
76
|
+
get_tracked(node: any): any;
|
|
77
|
+
get_computed(node: any): any;
|
|
78
|
+
set(node: any, value: any, block?: any): any;
|
|
79
|
+
// Add other runtime functions as needed for TypeScript analysis
|
|
80
|
+
};
|
|
81
|
+
}
|
|
File without changes
|