ripple 0.2.175 → 0.2.177
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 +3 -2
- package/shims/rollup-estree-types.d.ts +2 -0
- package/src/compiler/index.d.ts +30 -8
- package/src/compiler/index.js +10 -7
- package/src/compiler/phases/1-parse/index.js +117 -18
- package/src/compiler/phases/1-parse/style.js +4 -4
- package/src/compiler/phases/2-analyze/index.js +1 -0
- package/src/compiler/phases/3-transform/client/index.js +159 -22
- package/src/compiler/phases/3-transform/segments.js +123 -34
- package/src/compiler/types/index.d.ts +23 -0
- package/src/runtime/internal/client/events.js +18 -1
- package/src/utils/builders.js +49 -24
- package/tests/client/__snapshots__/tracked-expression.test.ripple.snap +1 -1
- package/tests/client/events.test.ripple +109 -1
- package/tests/client/media-query.test.ripple +5 -2
- package/tests/setup-client.js +4 -0
- package/tsconfig.json +2 -0
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.
|
|
6
|
+
"version": "0.2.177",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -77,11 +77,12 @@
|
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/estree": "^1.0.8",
|
|
80
|
+
"@types/estree-jsx": "^1.0.5",
|
|
80
81
|
"@types/node": "^24.3.0",
|
|
81
82
|
"typescript": "^5.9.2",
|
|
82
83
|
"@volar/language-core": "~2.4.23"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
85
|
-
"ripple": "0.2.
|
|
86
|
+
"ripple": "0.2.177"
|
|
86
87
|
}
|
|
87
88
|
}
|
package/src/compiler/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Program } from 'estree';
|
|
2
|
+
import type {
|
|
3
|
+
CodeInformation as VolarCodeInformation,
|
|
4
|
+
Mapping as VolarMapping,
|
|
5
|
+
} from '@volar/language-core';
|
|
2
6
|
|
|
3
7
|
// ============================================================================
|
|
4
8
|
// Compiler API Exports
|
|
@@ -18,12 +22,26 @@ export interface CompileResult {
|
|
|
18
22
|
css: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
export interface CustomMappingData {
|
|
26
|
+
generatedLengths: number[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MappingData extends VolarCodeInformation {
|
|
30
|
+
customData: CustomMappingData;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CodeMapping extends VolarMapping<MappingData> {
|
|
34
|
+
data: MappingData;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
/**
|
|
22
38
|
* Result of Volar mappings compilation
|
|
23
39
|
*/
|
|
24
40
|
export interface VolarMappingsResult {
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
code: string;
|
|
42
|
+
mappings: CodeMapping[];
|
|
43
|
+
cssMappings: CodeMapping[];
|
|
44
|
+
cssSources: string[];
|
|
27
45
|
}
|
|
28
46
|
|
|
29
47
|
/**
|
|
@@ -34,12 +52,18 @@ export interface CompileOptions {
|
|
|
34
52
|
mode?: 'client' | 'server';
|
|
35
53
|
}
|
|
36
54
|
|
|
55
|
+
export interface ParseOptions {
|
|
56
|
+
/** Enable loose mode */
|
|
57
|
+
loose?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
/**
|
|
38
61
|
* Parse Ripple source code to ESTree AST
|
|
39
62
|
* @param source - The Ripple source code to parse
|
|
63
|
+
* @param options - Parse options
|
|
40
64
|
* @returns The parsed ESTree Program AST
|
|
41
65
|
*/
|
|
42
|
-
export function parse(source: string): Program;
|
|
66
|
+
export function parse(source: string, options?: ParseOptions): Program;
|
|
43
67
|
|
|
44
68
|
/**
|
|
45
69
|
* Compile Ripple source code to JS/CSS output
|
|
@@ -48,19 +72,17 @@ export function parse(source: string): Program;
|
|
|
48
72
|
* @param options - Compilation options (mode: 'client' or 'server')
|
|
49
73
|
* @returns The compilation result with AST, JS, and CSS
|
|
50
74
|
*/
|
|
51
|
-
export function compile(
|
|
52
|
-
source: string,
|
|
53
|
-
filename: string,
|
|
54
|
-
options?: CompileOptions,
|
|
55
|
-
): CompileResult;
|
|
75
|
+
export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
|
|
56
76
|
|
|
57
77
|
/**
|
|
58
78
|
* Compile Ripple source to Volar mappings for editor integration
|
|
59
79
|
* @param source - The Ripple source code
|
|
60
80
|
* @param filename - The filename for source map generation
|
|
81
|
+
* @param options - Parse options
|
|
61
82
|
* @returns Volar mappings object for editor integration
|
|
62
83
|
*/
|
|
63
84
|
export function compile_to_volar_mappings(
|
|
64
85
|
source: string,
|
|
65
86
|
filename: string,
|
|
87
|
+
options?: ParseOptions,
|
|
66
88
|
): VolarMappingsResult;
|
package/src/compiler/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @import { Program } from 'estree' */
|
|
2
|
+
/** @import { ParseOptions } from 'ripple/compiler' */
|
|
2
3
|
|
|
3
4
|
import { parse as parse_module } from './phases/1-parse/index.js';
|
|
4
5
|
import { analyze } from './phases/2-analyze/index.js';
|
|
@@ -12,7 +13,7 @@ import { convert_source_map_to_mappings } from './phases/3-transform/segments.js
|
|
|
12
13
|
* @returns {Program}
|
|
13
14
|
*/
|
|
14
15
|
export function parse(source) {
|
|
15
|
-
return parse_module(source);
|
|
16
|
+
return parse_module(source, undefined);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -23,7 +24,7 @@ export function parse(source) {
|
|
|
23
24
|
* @returns {object}
|
|
24
25
|
*/
|
|
25
26
|
export function compile(source, filename, options = {}) {
|
|
26
|
-
const ast = parse_module(source);
|
|
27
|
+
const ast = parse_module(source, undefined);
|
|
27
28
|
const analysis = analyze(ast, filename, options);
|
|
28
29
|
const result =
|
|
29
30
|
options.mode === 'server'
|
|
@@ -34,22 +35,24 @@ export function compile(source, filename, options = {}) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
|
|
37
|
-
/** @import {
|
|
38
|
+
/** @import { VolarMappingsResult } from 'ripple/compiler' */
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Compile Ripple component to Volar virtual code with TypeScript mappings
|
|
41
42
|
* @param {string} source
|
|
42
43
|
* @param {string} filename
|
|
43
|
-
* @
|
|
44
|
+
* @param {ParseOptions} [options] - Compiler options
|
|
45
|
+
* @returns {VolarMappingsResult} Volar mappings object
|
|
44
46
|
*/
|
|
45
|
-
export function compile_to_volar_mappings(source, filename) {
|
|
46
|
-
const ast = parse_module(source);
|
|
47
|
-
const analysis = analyze(ast, filename, { to_ts: true });
|
|
47
|
+
export function compile_to_volar_mappings(source, filename, options) {
|
|
48
|
+
const ast = parse_module(source, options);
|
|
49
|
+
const analysis = analyze(ast, filename, { to_ts: true, loose: !!options?.loose });
|
|
48
50
|
const transformed = transform_client(filename, source, analysis, true);
|
|
49
51
|
|
|
50
52
|
// Create volar mappings with esrap source map for accurate positioning
|
|
51
53
|
return convert_source_map_to_mappings(
|
|
52
54
|
transformed.ast,
|
|
55
|
+
ast,
|
|
53
56
|
source,
|
|
54
57
|
transformed.js.code,
|
|
55
58
|
transformed.js.map,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/** @import { Program } from 'estree' */
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* @import {
|
|
4
5
|
* CommentWithLocation,
|
|
5
6
|
* RipplePluginConfig
|
|
6
7
|
* } from '#compiler' */
|
|
7
8
|
|
|
9
|
+
/** @import { ParseOptions } from 'ripple/compiler' */
|
|
10
|
+
|
|
8
11
|
import * as acorn from 'acorn';
|
|
9
12
|
import { tsPlugin } from '@sveltejs/acorn-typescript';
|
|
10
13
|
import { parse_style } from './style.js';
|
|
@@ -74,7 +77,7 @@ function isWhitespaceTextNode(node) {
|
|
|
74
77
|
* @returns {function(any): any} Parser extension function
|
|
75
78
|
*/
|
|
76
79
|
function RipplePlugin(config) {
|
|
77
|
-
return (/** @type {
|
|
80
|
+
return (/** @type {typeof acorn.Parser} */ Parser) => {
|
|
78
81
|
const original = acorn.Parser.prototype;
|
|
79
82
|
const tt = Parser.tokTypes || acorn.tokTypes;
|
|
80
83
|
const tc = Parser.tokContexts || acorn.tokContexts;
|
|
@@ -85,6 +88,12 @@ function RipplePlugin(config) {
|
|
|
85
88
|
/** @type {any[]} */
|
|
86
89
|
#path = [];
|
|
87
90
|
#commentContextId = 0;
|
|
91
|
+
#loose = false;
|
|
92
|
+
|
|
93
|
+
constructor(options, input) {
|
|
94
|
+
super(options, input);
|
|
95
|
+
this.#loose = options?.rippleOptions.loose === true;
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
#createCommentMetadata() {
|
|
90
99
|
if (this.#path.length === 0) {
|
|
@@ -1419,6 +1428,9 @@ function RipplePlugin(config) {
|
|
|
1419
1428
|
|
|
1420
1429
|
// Position after the '>'
|
|
1421
1430
|
this.pos = tagEndPos + 1;
|
|
1431
|
+
|
|
1432
|
+
// Add opening and closing for easier location tracking
|
|
1433
|
+
// TODO: we should also parse attributes inside the opening tag
|
|
1422
1434
|
} else {
|
|
1423
1435
|
open = this.jsx_parseOpeningElementAt();
|
|
1424
1436
|
}
|
|
@@ -1468,6 +1480,41 @@ function RipplePlugin(config) {
|
|
|
1468
1480
|
element.attributes = open.attributes;
|
|
1469
1481
|
element.metadata ??= {};
|
|
1470
1482
|
element.metadata.commentContainerId = ++this.#commentContextId;
|
|
1483
|
+
// Store opening tag's end position for use in loose mode when element is unclosed
|
|
1484
|
+
element.metadata.openingTagEnd = open.end;
|
|
1485
|
+
element.metadata.openingTagEndLoc = open.loc.end;
|
|
1486
|
+
|
|
1487
|
+
function addOpeningAndClosing() {
|
|
1488
|
+
//TODO: once parse attributes of style and script
|
|
1489
|
+
// we should the open element loc properly
|
|
1490
|
+
const name = open.name.name;
|
|
1491
|
+
open.loc = {
|
|
1492
|
+
start: {
|
|
1493
|
+
line: element.loc.start.line,
|
|
1494
|
+
column: element.loc.start.column,
|
|
1495
|
+
},
|
|
1496
|
+
end: {
|
|
1497
|
+
line: element.loc.start.line,
|
|
1498
|
+
column: element.loc.start.column + `${name}>`.length,
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
element.openingElement = open;
|
|
1503
|
+
element.closingElement = {
|
|
1504
|
+
type: 'JSXClosingElement',
|
|
1505
|
+
name: open.name,
|
|
1506
|
+
loc: {
|
|
1507
|
+
start: {
|
|
1508
|
+
line: element.loc.end.line,
|
|
1509
|
+
column: element.loc.end.column - `</${name}>`.length,
|
|
1510
|
+
},
|
|
1511
|
+
end: {
|
|
1512
|
+
line: element.loc.end.line,
|
|
1513
|
+
column: element.loc.end.column,
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1471
1518
|
|
|
1472
1519
|
if (element.selfClosing) {
|
|
1473
1520
|
this.#path.pop();
|
|
@@ -1506,6 +1553,7 @@ function RipplePlugin(config) {
|
|
|
1506
1553
|
|
|
1507
1554
|
element.content = content;
|
|
1508
1555
|
this.finishNode(element, 'Element');
|
|
1556
|
+
addOpeningAndClosing(element, open);
|
|
1509
1557
|
} else if (open.name.name === 'style') {
|
|
1510
1558
|
// jsx_parseOpeningElementAt treats ID selectors (ie. #myid) or type selectors (ie. div) as identifier and read it
|
|
1511
1559
|
// So backtrack to the end of the <style> tag to make sure everything is included
|
|
@@ -1515,7 +1563,7 @@ function RipplePlugin(config) {
|
|
|
1515
1563
|
const content = input.slice(0, end);
|
|
1516
1564
|
|
|
1517
1565
|
const component = this.#path.findLast((n) => n.type === 'Component');
|
|
1518
|
-
const parsed_css = parse_style(content);
|
|
1566
|
+
const parsed_css = parse_style(content, { loose: this.#loose });
|
|
1519
1567
|
|
|
1520
1568
|
if (!inside_head) {
|
|
1521
1569
|
if (component.css !== null) {
|
|
@@ -1553,6 +1601,7 @@ function RipplePlugin(config) {
|
|
|
1553
1601
|
|
|
1554
1602
|
element.css = content;
|
|
1555
1603
|
this.finishNode(element, 'Element');
|
|
1604
|
+
addOpeningAndClosing(element, open);
|
|
1556
1605
|
return element;
|
|
1557
1606
|
} else {
|
|
1558
1607
|
this.enterScope(0);
|
|
@@ -1590,15 +1639,24 @@ function RipplePlugin(config) {
|
|
|
1590
1639
|
this.next();
|
|
1591
1640
|
} else if (this.#path[this.#path.length - 1] === element) {
|
|
1592
1641
|
// Check if this element was properly closed
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1642
|
+
if (!this.#loose) {
|
|
1643
|
+
const tagName = this.getElementName(element.id);
|
|
1644
|
+
this.raise(
|
|
1645
|
+
this.start,
|
|
1646
|
+
`Unclosed tag '<${tagName}>'. Expected '</${tagName}>' before end of component.`,
|
|
1647
|
+
);
|
|
1648
|
+
} else {
|
|
1649
|
+
element.unclosed = true;
|
|
1650
|
+
const position = this.curPosition();
|
|
1651
|
+
position.line = element.metadata.openingTagEndLoc.line;
|
|
1652
|
+
position.column = element.metadata.openingTagEndLoc.column;
|
|
1653
|
+
element.loc.end = position;
|
|
1654
|
+
element.end = element.metadata.openingTagEnd;
|
|
1655
|
+
this.#path.pop();
|
|
1656
|
+
}
|
|
1600
1657
|
}
|
|
1601
1658
|
}
|
|
1659
|
+
|
|
1602
1660
|
// Ensure we escape JSX <tag></tag> context
|
|
1603
1661
|
const curContext = this.curContext();
|
|
1604
1662
|
|
|
@@ -1678,8 +1736,12 @@ function RipplePlugin(config) {
|
|
|
1678
1736
|
}
|
|
1679
1737
|
if (this.type === tt.braceL) {
|
|
1680
1738
|
const node = this.jsx_parseExpressionContainer();
|
|
1681
|
-
|
|
1682
|
-
|
|
1739
|
+
// Keep JSXEmptyExpression as-is (for prettier to handle comments)
|
|
1740
|
+
// but convert other expressions to Text/Html nodes
|
|
1741
|
+
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
1742
|
+
node.type = node.html ? 'Html' : 'Text';
|
|
1743
|
+
delete node.html;
|
|
1744
|
+
}
|
|
1683
1745
|
body.push(node);
|
|
1684
1746
|
} else if (this.type === tt.braceR) {
|
|
1685
1747
|
return;
|
|
@@ -1720,10 +1782,40 @@ function RipplePlugin(config) {
|
|
|
1720
1782
|
}
|
|
1721
1783
|
|
|
1722
1784
|
if (openingTagName !== closingTagName) {
|
|
1723
|
-
this
|
|
1724
|
-
this.
|
|
1725
|
-
|
|
1726
|
-
|
|
1785
|
+
if (!this.#loose) {
|
|
1786
|
+
this.raise(
|
|
1787
|
+
this.start,
|
|
1788
|
+
`Expected closing tag to match opening tag. Expected '</${openingTagName}>' but found '</${closingTagName}>'`,
|
|
1789
|
+
);
|
|
1790
|
+
} else {
|
|
1791
|
+
// Loop through all unclosed elements on the stack
|
|
1792
|
+
while (this.#path.length > 0) {
|
|
1793
|
+
const elem = this.#path[this.#path.length - 1];
|
|
1794
|
+
|
|
1795
|
+
// Stop at non-Element boundaries (Component, etc.)
|
|
1796
|
+
if (elem.type !== 'Element' && elem.type !== 'TsxCompat') {
|
|
1797
|
+
break;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const elemName =
|
|
1801
|
+
elem.type === 'TsxCompat' ? 'tsx:' + elem.kind : this.getElementName(elem.id);
|
|
1802
|
+
|
|
1803
|
+
// Found matching opening tag
|
|
1804
|
+
if (elemName === closingTagName) {
|
|
1805
|
+
break;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Mark as unclosed and adjust location
|
|
1809
|
+
elem.unclosed = true;
|
|
1810
|
+
const position = this.curPosition();
|
|
1811
|
+
position.line = elem.metadata.openingTagEndLoc.line;
|
|
1812
|
+
position.column = elem.metadata.openingTagEndLoc.column;
|
|
1813
|
+
elem.loc.end = position;
|
|
1814
|
+
elem.end = elem.metadata.openingTagEnd;
|
|
1815
|
+
|
|
1816
|
+
this.#path.pop(); // Remove from stack
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1727
1819
|
}
|
|
1728
1820
|
|
|
1729
1821
|
this.#path.pop();
|
|
@@ -1759,7 +1851,10 @@ function RipplePlugin(config) {
|
|
|
1759
1851
|
) {
|
|
1760
1852
|
this.next();
|
|
1761
1853
|
const node = this.jsx_parseExpressionContainer();
|
|
1762
|
-
|
|
1854
|
+
// Keep JSXEmptyExpression as-is (don't convert to Text)
|
|
1855
|
+
if (node.expression.type !== 'JSXEmptyExpression') {
|
|
1856
|
+
node.type = 'Text';
|
|
1857
|
+
}
|
|
1763
1858
|
this.next();
|
|
1764
1859
|
this.context.pop();
|
|
1765
1860
|
this.context.pop();
|
|
@@ -2256,9 +2351,10 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
2256
2351
|
/**
|
|
2257
2352
|
* Parse Ripple source code into an AST
|
|
2258
2353
|
* @param {string} source
|
|
2354
|
+
* @param {ParseOptions} [options]
|
|
2259
2355
|
* @returns {Program}
|
|
2260
2356
|
*/
|
|
2261
|
-
export function parse(source) {
|
|
2357
|
+
export function parse(source, options) {
|
|
2262
2358
|
/** @type {CommentWithLocation[]} */
|
|
2263
2359
|
const comments = [];
|
|
2264
2360
|
|
|
@@ -2315,6 +2411,9 @@ export function parse(source) {
|
|
|
2315
2411
|
ecmaVersion: 13,
|
|
2316
2412
|
locations: true,
|
|
2317
2413
|
onComment: /** @type {any} */ (onComment),
|
|
2414
|
+
rippleOptions: {
|
|
2415
|
+
loose: options?.loose || false,
|
|
2416
|
+
},
|
|
2318
2417
|
});
|
|
2319
2418
|
} catch (e) {
|
|
2320
2419
|
throw e;
|
|
@@ -88,8 +88,8 @@ class Parser {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function parse_style(content) {
|
|
92
|
-
const parser = new Parser(content, false);
|
|
91
|
+
export function parse_style(content, options) {
|
|
92
|
+
const parser = new Parser(content, options.loose || false);
|
|
93
93
|
|
|
94
94
|
return {
|
|
95
95
|
source: content,
|
|
@@ -232,8 +232,8 @@ function read_declaration(parser) {
|
|
|
232
232
|
|
|
233
233
|
const value = read_value(parser);
|
|
234
234
|
|
|
235
|
-
if (!value && !property.startsWith('--')) {
|
|
236
|
-
|
|
235
|
+
if (!value && !property.startsWith('--') && !parser.loose) {
|
|
236
|
+
throw new Error('CSS Declaration cannot be empty');
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
const end = parser.index;
|