ripple 0.2.91 → 0.2.93
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/index.js +17 -10
- package/src/compiler/phases/1-parse/index.js +55 -7
- package/src/compiler/phases/3-transform/client/index.js +82 -55
- package/src/compiler/phases/3-transform/segments.js +422 -224
- package/src/compiler/scope.js +478 -404
- package/src/compiler/types/index.d.ts +299 -3
- package/src/compiler/utils.js +173 -30
- package/src/runtime/index-client.js +1 -0
- package/src/runtime/internal/client/html.js +18 -8
- package/src/runtime/internal/client/index.js +1 -0
- package/src/runtime/internal/client/portal.js +55 -32
- package/src/runtime/internal/client/render.js +31 -1
- package/src/runtime/internal/client/runtime.js +53 -22
- package/src/utils/normalize_css_property_name.js +23 -0
- package/tests/client/basic.test.ripple +207 -1
- package/tests/client/compiler.test.ripple +95 -1
- package/tests/client/html.test.ripple +29 -1
- package/tests/client/portal.test.ripple +167 -0
- package/types/index.d.ts +4 -0
package/package.json
CHANGED
package/src/compiler/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/** @import { RawSourceMap } from 'source-map' */
|
|
2
1
|
/** @import { Program } from 'estree' */
|
|
3
|
-
/** @import { ParseError } from './phases/1-parse/index.js' */
|
|
4
2
|
|
|
5
3
|
import { parse as parse_module } from './phases/1-parse/index.js';
|
|
6
4
|
import { analyze } from './phases/2-analyze/index.js';
|
|
@@ -9,18 +7,20 @@ import { transform_server } from './phases/3-transform/server/index.js';
|
|
|
9
7
|
import { convert_source_map_to_mappings } from './phases/3-transform/segments.js';
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
10
|
+
* Parse Ripple source code to ESTree AST
|
|
11
|
+
* @param {string} source
|
|
12
|
+
* @returns {Program}
|
|
14
13
|
*/
|
|
15
14
|
export function parse(source) {
|
|
16
15
|
return parse_module(source);
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @param {string}
|
|
22
|
-
* @param {
|
|
23
|
-
* @
|
|
19
|
+
* Compile Ripple source code to JS/CSS output
|
|
20
|
+
* @param {string} source
|
|
21
|
+
* @param {string} filename
|
|
22
|
+
* @param {{ mode?: 'client' | 'server' }} [options]
|
|
23
|
+
* @returns {object}
|
|
24
24
|
*/
|
|
25
25
|
export function compile(source, filename, options = {}) {
|
|
26
26
|
const ast = parse_module(source);
|
|
@@ -32,11 +32,18 @@ export function compile(source, filename, options = {}) {
|
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Compile Ripple source to Volar mappings for editor integration
|
|
37
|
+
* @param {string} source
|
|
38
|
+
* @param {string} filename
|
|
39
|
+
* @returns {object} Volar mappings object
|
|
40
|
+
*/
|
|
35
41
|
export function compile_to_volar_mappings(source, filename) {
|
|
36
|
-
// Parse and transform
|
|
42
|
+
// Parse and transform
|
|
37
43
|
const ast = parse_module(source);
|
|
38
44
|
const analysis = analyze(ast, filename);
|
|
39
45
|
const transformed = transform_client(filename, source, analysis, true);
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
// Create volar mappings directly from the AST instead of relying on esrap's sourcemap
|
|
48
|
+
return convert_source_map_to_mappings(transformed.ast, source, transformed.js.code);
|
|
42
49
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** @import { Program } from 'estree' */
|
|
2
|
+
/** @import {
|
|
3
|
+
* CommentWithLocation,
|
|
4
|
+
* RipplePluginConfig
|
|
5
|
+
* } from '#compiler' */
|
|
6
|
+
|
|
1
7
|
import * as acorn from 'acorn';
|
|
2
8
|
import { tsPlugin } from 'acorn-typescript';
|
|
3
9
|
import { parse_style } from './style.js';
|
|
@@ -6,6 +12,11 @@ import { regex_newline_characters } from '../../../utils/patterns.js';
|
|
|
6
12
|
|
|
7
13
|
const parser = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }), RipplePlugin());
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Convert JSX node types to regular JavaScript node types
|
|
17
|
+
* @param {any} node - The JSX node to convert
|
|
18
|
+
* @returns {any} The converted node
|
|
19
|
+
*/
|
|
9
20
|
function convert_from_jsx(node) {
|
|
10
21
|
if (node.type === 'JSXIdentifier') {
|
|
11
22
|
node.type = 'Identifier';
|
|
@@ -17,16 +28,26 @@ function convert_from_jsx(node) {
|
|
|
17
28
|
return node;
|
|
18
29
|
}
|
|
19
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Acorn parser plugin for Ripple syntax extensions
|
|
33
|
+
* @param {RipplePluginConfig} [config] - Plugin configuration
|
|
34
|
+
* @returns {function(any): any} Parser extension function
|
|
35
|
+
*/
|
|
20
36
|
function RipplePlugin(config) {
|
|
21
|
-
return (Parser) => {
|
|
37
|
+
return (/** @type {any} */ Parser) => {
|
|
22
38
|
const original = acorn.Parser.prototype;
|
|
23
39
|
const tt = Parser.tokTypes || acorn.tokTypes;
|
|
24
40
|
const tc = Parser.tokContexts || acorn.tokContexts;
|
|
25
41
|
|
|
26
42
|
class RippleParser extends Parser {
|
|
43
|
+
/** @type {any[]} */
|
|
27
44
|
#path = [];
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Helper method to get the element name from a JSX identifier or member expression
|
|
48
|
+
* @param {any} node - The node to get the name from
|
|
49
|
+
* @returns {string | null} Element name or null
|
|
50
|
+
*/
|
|
30
51
|
getElementName(node) {
|
|
31
52
|
if (!node) return null;
|
|
32
53
|
if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
|
|
@@ -38,6 +59,11 @@ function RipplePlugin(config) {
|
|
|
38
59
|
return null;
|
|
39
60
|
}
|
|
40
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get token from character code - handles Ripple-specific tokens
|
|
64
|
+
* @param {number} code - Character code
|
|
65
|
+
* @returns {any} Token or calls super method
|
|
66
|
+
*/
|
|
41
67
|
getTokenFromCode(code) {
|
|
42
68
|
if (code === 60) {
|
|
43
69
|
// < character
|
|
@@ -135,7 +161,10 @@ function RipplePlugin(config) {
|
|
|
135
161
|
return super.getTokenFromCode(code);
|
|
136
162
|
}
|
|
137
163
|
|
|
138
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Read an @ prefixed identifier
|
|
166
|
+
* @returns {any} Token with @ identifier
|
|
167
|
+
*/
|
|
139
168
|
readAtIdentifier() {
|
|
140
169
|
const start = this.pos;
|
|
141
170
|
this.pos++; // skip '@'
|
|
@@ -166,7 +195,11 @@ function RipplePlugin(config) {
|
|
|
166
195
|
return this.finishToken(tt.name, '@' + word);
|
|
167
196
|
}
|
|
168
197
|
|
|
169
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Override parseIdent to mark @ identifiers as tracked
|
|
200
|
+
* @param {any} [liberal] - Whether to allow liberal parsing
|
|
201
|
+
* @returns {any} Parsed identifier node
|
|
202
|
+
*/
|
|
170
203
|
parseIdent(liberal) {
|
|
171
204
|
const node = super.parseIdent(liberal);
|
|
172
205
|
if (node.name && node.name.startsWith('@')) {
|
|
@@ -181,6 +214,13 @@ function RipplePlugin(config) {
|
|
|
181
214
|
return node;
|
|
182
215
|
}
|
|
183
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Parse expression atom - handles TrackedArray and TrackedObject literals
|
|
219
|
+
* @param {any} [refDestructuringErrors]
|
|
220
|
+
* @param {any} [forNew]
|
|
221
|
+
* @param {any} [forInit]
|
|
222
|
+
* @returns {any} Parsed expression atom
|
|
223
|
+
*/
|
|
184
224
|
parseExprAtom(refDestructuringErrors, forNew, forInit) {
|
|
185
225
|
// Check if this is a tuple literal starting with #[
|
|
186
226
|
if (this.type === tt.bracketL && this.value === '#[') {
|
|
@@ -818,6 +858,7 @@ function RipplePlugin(config) {
|
|
|
818
858
|
const element = this.startNode();
|
|
819
859
|
element.start = position.index;
|
|
820
860
|
element.loc.start = position;
|
|
861
|
+
element.metadata = {};
|
|
821
862
|
element.type = 'Element';
|
|
822
863
|
this.#path.push(element);
|
|
823
864
|
element.children = [];
|
|
@@ -1150,7 +1191,8 @@ function RipplePlugin(config) {
|
|
|
1150
1191
|
* in JS code and so that `prettier-plugin-ripple` doesn't remove all comments when formatting.
|
|
1151
1192
|
* @param {string} source
|
|
1152
1193
|
* @param {CommentWithLocation[]} comments
|
|
1153
|
-
* @param {number} index
|
|
1194
|
+
* @param {number} [index=0] - Starting index
|
|
1195
|
+
* @returns {{ onComment: Function, add_comments: Function }} Comment handler functions
|
|
1154
1196
|
*/
|
|
1155
1197
|
function get_comment_handlers(source, comments, index = 0) {
|
|
1156
1198
|
return {
|
|
@@ -1245,7 +1287,13 @@ function get_comment_handlers(source, comments, index = 0) {
|
|
|
1245
1287
|
};
|
|
1246
1288
|
}
|
|
1247
1289
|
|
|
1290
|
+
/**
|
|
1291
|
+
* Parse Ripple source code into an AST
|
|
1292
|
+
* @param {string} source
|
|
1293
|
+
* @returns {Program}
|
|
1294
|
+
*/
|
|
1248
1295
|
export function parse(source) {
|
|
1296
|
+
/** @type {CommentWithLocation[]} */
|
|
1249
1297
|
const comments = [];
|
|
1250
1298
|
const { onComment, add_comments } = get_comment_handlers(source, comments);
|
|
1251
1299
|
let ast;
|
|
@@ -1255,7 +1303,7 @@ export function parse(source) {
|
|
|
1255
1303
|
sourceType: 'module',
|
|
1256
1304
|
ecmaVersion: 13,
|
|
1257
1305
|
locations: true,
|
|
1258
|
-
onComment,
|
|
1306
|
+
onComment: /** @type {any} */ (onComment),
|
|
1259
1307
|
});
|
|
1260
1308
|
} catch (e) {
|
|
1261
1309
|
throw e;
|
|
@@ -1263,5 +1311,5 @@ export function parse(source) {
|
|
|
1263
1311
|
|
|
1264
1312
|
add_comments(ast);
|
|
1265
1313
|
|
|
1266
|
-
return ast;
|
|
1314
|
+
return /** @type {Program} */ (ast);
|
|
1267
1315
|
}
|
|
@@ -544,6 +544,7 @@ const visitors = {
|
|
|
544
544
|
|
|
545
545
|
if (is_dom_element) {
|
|
546
546
|
let class_attribute = null;
|
|
547
|
+
let style_attribute = null;
|
|
547
548
|
const local_updates = [];
|
|
548
549
|
const is_void = is_void_element(node.id.name);
|
|
549
550
|
|
|
@@ -559,7 +560,7 @@ const visitors = {
|
|
|
559
560
|
continue;
|
|
560
561
|
}
|
|
561
562
|
|
|
562
|
-
if (attr.value.type === 'Literal' && name !== 'class') {
|
|
563
|
+
if (attr.value.type === 'Literal' && name !== 'class' && name !== 'style') {
|
|
563
564
|
handle_static_attr(name, attr.value.value);
|
|
564
565
|
continue;
|
|
565
566
|
}
|
|
@@ -570,6 +571,12 @@ const visitors = {
|
|
|
570
571
|
continue;
|
|
571
572
|
}
|
|
572
573
|
|
|
574
|
+
if (name === 'style') {
|
|
575
|
+
style_attribute = attr;
|
|
576
|
+
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
573
580
|
if (name === 'value') {
|
|
574
581
|
const id = state.flush_node();
|
|
575
582
|
const metadata = { tracking: false, await: false };
|
|
@@ -742,6 +749,25 @@ const visitors = {
|
|
|
742
749
|
handle_static_attr(is_spreading ? '#class' : 'class', value);
|
|
743
750
|
}
|
|
744
751
|
|
|
752
|
+
if (style_attribute !== null) {
|
|
753
|
+
if (style_attribute.value.type === 'Literal') {
|
|
754
|
+
handle_static_attr(style_attribute.name.name, style_attribute.value.value);
|
|
755
|
+
} else {
|
|
756
|
+
const id = state.flush_node();
|
|
757
|
+
const metadata = { tracking: false, await: false };
|
|
758
|
+
const expression = visit(style_attribute.value, { ...state, metadata });
|
|
759
|
+
const name = style_attribute.name.name;
|
|
760
|
+
|
|
761
|
+
const statement = b.stmt(b.call('_$_.set_attribute', id, b.literal(name), expression));
|
|
762
|
+
|
|
763
|
+
if (metadata.tracking) {
|
|
764
|
+
local_updates.push(statement);
|
|
765
|
+
} else {
|
|
766
|
+
state.init.push(statement);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
745
771
|
state.template.push('>');
|
|
746
772
|
|
|
747
773
|
if (spread_attributes !== null && spread_attributes.length > 0) {
|
|
@@ -799,13 +825,17 @@ const visitors = {
|
|
|
799
825
|
}
|
|
800
826
|
|
|
801
827
|
props.push(
|
|
802
|
-
b.prop(
|
|
828
|
+
b.prop(
|
|
829
|
+
'get',
|
|
830
|
+
b.key(attr.name.name),
|
|
831
|
+
b.function(null, [], b.block([b.return(property)])),
|
|
832
|
+
),
|
|
803
833
|
);
|
|
804
834
|
} else {
|
|
805
|
-
props.push(b.prop('init', attr.name, property));
|
|
835
|
+
props.push(b.prop('init', b.key(attr.name.name), property));
|
|
806
836
|
}
|
|
807
837
|
} else {
|
|
808
|
-
props.push(b.prop('init', attr.name, visit(attr.value, state)));
|
|
838
|
+
props.push(b.prop('init', b.key(attr.name.name), visit(attr.value, state)));
|
|
809
839
|
}
|
|
810
840
|
} else if (attr.type === 'SpreadAttribute') {
|
|
811
841
|
props.push(
|
|
@@ -1317,54 +1347,46 @@ function transform_ts_child(node, context) {
|
|
|
1317
1347
|
const children = [];
|
|
1318
1348
|
let has_children_props = false;
|
|
1319
1349
|
|
|
1320
|
-
// Filter out RefAttributes and handle them separately
|
|
1321
1350
|
const ref_attributes = [];
|
|
1322
|
-
const attributes = node.attributes
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1351
|
+
const attributes = node.attributes.map((attr) => {
|
|
1352
|
+
if (attr.type === 'Attribute') {
|
|
1353
|
+
const metadata = { await: false };
|
|
1354
|
+
const name = visit(attr.name, { ...state, metadata });
|
|
1355
|
+
const value =
|
|
1356
|
+
attr.value === null ? b.literal(true) : visit(attr.value, { ...state, metadata });
|
|
1357
|
+
|
|
1358
|
+
// Handle both regular identifiers and tracked identifiers
|
|
1359
|
+
let prop_name;
|
|
1360
|
+
if (name.type === 'Identifier') {
|
|
1361
|
+
prop_name = name.name;
|
|
1362
|
+
} else if (name.type === 'MemberExpression' && name.object.type === 'Identifier') {
|
|
1363
|
+
// For tracked attributes like {@count}, use the original name
|
|
1364
|
+
prop_name = name.object.name;
|
|
1365
|
+
} else {
|
|
1366
|
+
prop_name = attr.name.name || 'unknown';
|
|
1327
1367
|
}
|
|
1328
|
-
return true;
|
|
1329
|
-
})
|
|
1330
|
-
.map((attr) => {
|
|
1331
|
-
if (attr.type === 'Attribute') {
|
|
1332
|
-
const metadata = { await: false };
|
|
1333
|
-
const name = visit(attr.name, { ...state, metadata });
|
|
1334
|
-
const value =
|
|
1335
|
-
attr.value === null ? b.literal(true) : visit(attr.value, { ...state, metadata });
|
|
1336
|
-
|
|
1337
|
-
// Handle both regular identifiers and tracked identifiers
|
|
1338
|
-
let prop_name;
|
|
1339
|
-
if (name.type === 'Identifier') {
|
|
1340
|
-
prop_name = name.name;
|
|
1341
|
-
} else if (name.type === 'MemberExpression' && name.object.type === 'Identifier') {
|
|
1342
|
-
// For tracked attributes like {@count}, use the original name
|
|
1343
|
-
prop_name = name.object.name;
|
|
1344
|
-
} else {
|
|
1345
|
-
prop_name = attr.name.name || 'unknown';
|
|
1346
|
-
}
|
|
1347
1368
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
jsx_name.loc = attr.name.loc || name.loc;
|
|
1353
|
-
|
|
1354
|
-
return b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
|
|
1355
|
-
} else if (attr.type === 'SpreadAttribute') {
|
|
1356
|
-
const metadata = { await: false };
|
|
1357
|
-
const argument = visit(attr.argument, { ...state, metadata });
|
|
1358
|
-
return b.jsx_spread_attribute(argument);
|
|
1369
|
+
const jsx_name = b.jsx_id(prop_name);
|
|
1370
|
+
if (prop_name === 'children') {
|
|
1371
|
+
has_children_props = true;
|
|
1359
1372
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1373
|
+
jsx_name.loc = attr.name.loc || name.loc;
|
|
1374
|
+
|
|
1375
|
+
return b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
|
|
1376
|
+
} else if (attr.type === 'SpreadAttribute') {
|
|
1377
|
+
const metadata = { await: false };
|
|
1378
|
+
const argument = visit(attr.argument, { ...state, metadata });
|
|
1379
|
+
return b.jsx_spread_attribute(argument);
|
|
1380
|
+
} else if (attr.type === 'RefAttribute') {
|
|
1381
|
+
if (!context.state.imports.has(`import { createRefKey } from 'ripple'`)) {
|
|
1382
|
+
context.state.imports.add(`import { createRefKey } from 'ripple'`);
|
|
1383
|
+
}
|
|
1384
|
+
const metadata = { await: false };
|
|
1385
|
+
const argument = visit(attr.argument, { ...state, metadata });
|
|
1386
|
+
const wrapper = b.object([b.prop('init', b.call('createRefKey'), argument, true)]);
|
|
1387
|
+
return b.jsx_spread_attribute(wrapper);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1368
1390
|
|
|
1369
1391
|
if (!node.selfClosing && !has_children_props && node.children.length > 0) {
|
|
1370
1392
|
const is_dom_element = is_element_dom_element(node);
|
|
@@ -1382,12 +1404,7 @@ function transform_ts_child(node, context) {
|
|
|
1382
1404
|
if (is_dom_element) {
|
|
1383
1405
|
children.push(b.jsx_expression_container(b.call(thunk)));
|
|
1384
1406
|
} else {
|
|
1385
|
-
|
|
1386
|
-
const children_id = b.id(children_name);
|
|
1387
|
-
const jsx_id = b.jsx_id('children');
|
|
1388
|
-
jsx_id.loc = node.id.loc;
|
|
1389
|
-
state.init.push(b.const(children_id, thunk));
|
|
1390
|
-
attributes.push(b.jsx_attribute(jsx_id, b.jsx_expression_container(children_id)));
|
|
1407
|
+
attributes.push(b.jsx_attribute(b.jsx_id('children'), b.jsx_expression_container(thunk)));
|
|
1391
1408
|
}
|
|
1392
1409
|
}
|
|
1393
1410
|
|
|
@@ -1607,7 +1624,17 @@ function transform_children(children, context) {
|
|
|
1607
1624
|
context.state.template.push('<!>');
|
|
1608
1625
|
|
|
1609
1626
|
const id = flush_node();
|
|
1610
|
-
state.update.push(
|
|
1627
|
+
state.update.push(
|
|
1628
|
+
b.stmt(
|
|
1629
|
+
b.call(
|
|
1630
|
+
'_$_.html',
|
|
1631
|
+
id,
|
|
1632
|
+
b.thunk(expression),
|
|
1633
|
+
state.namespace === 'svg' && b.true,
|
|
1634
|
+
state.namespace === 'mathml' && b.true,
|
|
1635
|
+
),
|
|
1636
|
+
),
|
|
1637
|
+
);
|
|
1611
1638
|
} else if (node.type === 'Text') {
|
|
1612
1639
|
const metadata = { tracking: false, await: false };
|
|
1613
1640
|
const expression = visit(node.expression, { ...state, metadata });
|