ripple 0.2.90 → 0.2.92

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 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.90",
6
+ "version": "0.2.92",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -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
- * @param {string} source
13
- * @returns {{ ast: Program, errors: ParseError[] }}
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
- * @param {string} source
21
- * @param {string} filename
22
- * @param {{ mode?: 'client' | 'server' }} options
23
- * @returns {{ js: { code: string, map: RawSourceMap }, css: { code: string, map: RawSourceMap } | null }}
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,6 +32,12 @@ 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
42
  // Parse and transform to get the esrap sourcemap
37
43
  const ast = parse_module(source);
@@ -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
- // Helper method to get the element name from a JSX identifier or member expression
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
- // Read an @ prefixed identifier
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
- // Override parseIdent to mark @ identifiers as tracked
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 === '#[') {
@@ -455,24 +495,30 @@ function RipplePlugin(config) {
455
495
  jsx_parseExpressionContainer() {
456
496
  let node = this.startNode();
457
497
  this.next();
458
- let tracked = false;
498
+ let tracked = false;
459
499
 
460
- if (this.value === 'html') {
461
- node.html = true;
462
- this.next();
463
- if (this.type.label === '@') {
464
- this.next(); // consume @
465
- tracked = true;
466
- }
467
- }
500
+ if (this.value === 'html') {
501
+ node.html = true;
502
+ this.next();
503
+ if (this.type === tt.braceR) {
504
+ this.raise(
505
+ this.start,
506
+ '"html" is a Ripple keyword and must be used in the form {html some_content}',
507
+ );
508
+ }
509
+ if (this.type.label === '@') {
510
+ this.next(); // consume @
511
+ tracked = true;
512
+ }
513
+ }
468
514
 
469
515
  node.expression =
470
516
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
471
517
  this.expect(tt.braceR);
472
518
 
473
- if (tracked && node.expression.type === 'Identifier') {
474
- node.expression.tracked = true;
475
- }
519
+ if (tracked && node.expression.type === 'Identifier') {
520
+ node.expression.tracked = true;
521
+ }
476
522
 
477
523
  return this.finishNode(node, 'JSXExpressionContainer');
478
524
  }
@@ -812,6 +858,7 @@ function RipplePlugin(config) {
812
858
  const element = this.startNode();
813
859
  element.start = position.index;
814
860
  element.loc.start = position;
861
+ element.metadata = {};
815
862
  element.type = 'Element';
816
863
  this.#path.push(element);
817
864
  element.children = [];
@@ -970,7 +1017,7 @@ function RipplePlugin(config) {
970
1017
  if (this.type.label === '{') {
971
1018
  const node = this.jsx_parseExpressionContainer();
972
1019
  node.type = node.html ? 'Html' : 'Text';
973
- delete node.html;
1020
+ delete node.html;
974
1021
  body.push(node);
975
1022
  } else if (this.type.label === '}') {
976
1023
  return;
@@ -1144,7 +1191,8 @@ function RipplePlugin(config) {
1144
1191
  * in JS code and so that `prettier-plugin-ripple` doesn't remove all comments when formatting.
1145
1192
  * @param {string} source
1146
1193
  * @param {CommentWithLocation[]} comments
1147
- * @param {number} index
1194
+ * @param {number} [index=0] - Starting index
1195
+ * @returns {{ onComment: Function, add_comments: Function }} Comment handler functions
1148
1196
  */
1149
1197
  function get_comment_handlers(source, comments, index = 0) {
1150
1198
  return {
@@ -1239,7 +1287,13 @@ function get_comment_handlers(source, comments, index = 0) {
1239
1287
  };
1240
1288
  }
1241
1289
 
1290
+ /**
1291
+ * Parse Ripple source code into an AST
1292
+ * @param {string} source
1293
+ * @returns {Program}
1294
+ */
1242
1295
  export function parse(source) {
1296
+ /** @type {CommentWithLocation[]} */
1243
1297
  const comments = [];
1244
1298
  const { onComment, add_comments } = get_comment_handlers(source, comments);
1245
1299
  let ast;
@@ -1249,7 +1303,7 @@ export function parse(source) {
1249
1303
  sourceType: 'module',
1250
1304
  ecmaVersion: 13,
1251
1305
  locations: true,
1252
- onComment,
1306
+ onComment: /** @type {any} */ (onComment),
1253
1307
  });
1254
1308
  } catch (e) {
1255
1309
  throw e;
@@ -1257,5 +1311,5 @@ export function parse(source) {
1257
1311
 
1258
1312
  add_comments(ast);
1259
1313
 
1260
- return ast;
1314
+ return /** @type {Program} */ (ast);
1261
1315
  }
@@ -46,8 +46,7 @@ function add_ripple_internal_import(context) {
46
46
 
47
47
  function visit_function(node, context) {
48
48
  if (context.state.to_ts) {
49
- context.next(context.state);
50
- return;
49
+ return context.next(context.state);
51
50
  }
52
51
  const metadata = node.metadata;
53
52
  const state = context.state;
@@ -545,6 +544,7 @@ const visitors = {
545
544
 
546
545
  if (is_dom_element) {
547
546
  let class_attribute = null;
547
+ let style_attribute = null;
548
548
  const local_updates = [];
549
549
  const is_void = is_void_element(node.id.name);
550
550
 
@@ -560,7 +560,7 @@ const visitors = {
560
560
  continue;
561
561
  }
562
562
 
563
- if (attr.value.type === 'Literal' && name !== 'class') {
563
+ if (attr.value.type === 'Literal' && name !== 'class' && name !== 'style') {
564
564
  handle_static_attr(name, attr.value.value);
565
565
  continue;
566
566
  }
@@ -571,6 +571,12 @@ const visitors = {
571
571
  continue;
572
572
  }
573
573
 
574
+ if (name === 'style') {
575
+ style_attribute = attr;
576
+
577
+ continue;
578
+ }
579
+
574
580
  if (name === 'value') {
575
581
  const id = state.flush_node();
576
582
  const metadata = { tracking: false, await: false };
@@ -743,6 +749,25 @@ const visitors = {
743
749
  handle_static_attr(is_spreading ? '#class' : 'class', value);
744
750
  }
745
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
+
746
771
  state.template.push('>');
747
772
 
748
773
  if (spread_attributes !== null && spread_attributes.length > 0) {
@@ -800,13 +825,17 @@ const visitors = {
800
825
  }
801
826
 
802
827
  props.push(
803
- b.prop('get', attr.name, b.function(null, [], b.block([b.return(property)]))),
828
+ b.prop(
829
+ 'get',
830
+ b.key(attr.name.name),
831
+ b.function(null, [], b.block([b.return(property)])),
832
+ ),
804
833
  );
805
834
  } else {
806
- props.push(b.prop('init', attr.name, property));
835
+ props.push(b.prop('init', b.key(attr.name.name), property));
807
836
  }
808
837
  } else {
809
- 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)));
810
839
  }
811
840
  } else if (attr.type === 'SpreadAttribute') {
812
841
  props.push(
@@ -896,7 +925,11 @@ const visitors = {
896
925
  }),
897
926
  ];
898
927
 
899
- return b.function(node.id, node.params, b.block(body_statements));
928
+ return b.function(
929
+ node.id,
930
+ node.params.map((param) => context.visit(param, { ...context.state, metadata })),
931
+ b.block(body_statements),
932
+ );
900
933
  }
901
934
 
902
935
  let props = b.id('__props');
@@ -1000,8 +1033,7 @@ const visitors = {
1000
1033
 
1001
1034
  UpdateExpression(node, context) {
1002
1035
  if (context.state.to_ts) {
1003
- context.next();
1004
- return;
1036
+ return context.next();
1005
1037
  }
1006
1038
  const argument = node.argument;
1007
1039
 
@@ -1047,8 +1079,7 @@ const visitors = {
1047
1079
 
1048
1080
  ForOfStatement(node, context) {
1049
1081
  if (!is_inside_component(context)) {
1050
- context.next();
1051
- return;
1082
+ return context.next();
1052
1083
  }
1053
1084
  const is_controlled = node.is_controlled;
1054
1085
  const index = node.index;
@@ -1090,8 +1121,7 @@ const visitors = {
1090
1121
 
1091
1122
  IfStatement(node, context) {
1092
1123
  if (!is_inside_component(context)) {
1093
- context.next();
1094
- return;
1124
+ return context.next();
1095
1125
  }
1096
1126
  context.state.template.push('<!>');
1097
1127
 
@@ -1174,8 +1204,7 @@ const visitors = {
1174
1204
 
1175
1205
  TryStatement(node, context) {
1176
1206
  if (!is_inside_component(context)) {
1177
- context.next();
1178
- return;
1207
+ return context.next();
1179
1208
  }
1180
1209
  context.state.template.push('<!>');
1181
1210
 
@@ -1310,46 +1339,54 @@ function transform_ts_child(node, context) {
1310
1339
 
1311
1340
  if (node.type === 'Text') {
1312
1341
  state.init.push(b.stmt(visit(node.expression, { ...state })));
1342
+ } else if (node.type === 'Html') {
1343
+ // Do we need to do something special here?
1344
+ state.init.push(b.stmt(visit(node.expression, { ...state })));
1313
1345
  } else if (node.type === 'Element') {
1314
1346
  const type = node.id.name;
1315
1347
  const children = [];
1316
1348
  let has_children_props = false;
1317
1349
 
1318
- // Filter out RefAttributes and handle them separately
1319
1350
  const ref_attributes = [];
1320
- const attributes = node.attributes
1321
- .filter((attr) => {
1322
- if (attr.type === 'RefAttribute') {
1323
- ref_attributes.push(attr);
1324
- return false;
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';
1325
1367
  }
1326
- return true;
1327
- })
1328
- .map((attr) => {
1329
- if (attr.type === 'Attribute') {
1330
- const metadata = { await: false };
1331
- const name = visit(attr.name, { ...state, metadata });
1332
- const value = visit(attr.value, { ...state, metadata });
1333
- const jsx_name = b.jsx_id(name.name);
1334
- if (name.name === 'children') {
1335
- has_children_props = true;
1336
- }
1337
- jsx_name.loc = name.loc;
1338
1368
 
1339
- return b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
1340
- } else if (attr.type === 'SpreadAttribute') {
1341
- const metadata = { await: false };
1342
- const argument = visit(attr.argument, { ...state, metadata });
1343
- 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;
1344
1372
  }
1345
- });
1346
-
1347
- // Add RefAttribute references separately for sourcemap purposes
1348
- for (const ref_attr of ref_attributes) {
1349
- const metadata = { await: false };
1350
- const argument = visit(ref_attr.argument, { ...state, metadata });
1351
- state.init.push(b.stmt(argument));
1352
- }
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
+ });
1353
1390
 
1354
1391
  if (!node.selfClosing && !has_children_props && node.children.length > 0) {
1355
1392
  const is_dom_element = is_element_dom_element(node);
@@ -1468,7 +1505,7 @@ function transform_ts_child(node, context) {
1468
1505
 
1469
1506
  state.init.push(b.try(try_body, catch_handler, finally_block));
1470
1507
  } else if (node.type === 'Component') {
1471
- const component = visit(node, context.state);
1508
+ const component = visit(node, state);
1472
1509
 
1473
1510
  state.init.push(component);
1474
1511
  } else {
@@ -1592,7 +1629,17 @@ function transform_children(children, context) {
1592
1629
  context.state.template.push('<!>');
1593
1630
 
1594
1631
  const id = flush_node();
1595
- state.update.push(b.stmt(b.call('_$_.html', id, b.thunk(expression))));
1632
+ state.update.push(
1633
+ b.stmt(
1634
+ b.call(
1635
+ '_$_.html',
1636
+ id,
1637
+ b.thunk(expression),
1638
+ state.namespace === 'svg' && b.true,
1639
+ state.namespace === 'mathml' && b.true,
1640
+ ),
1641
+ ),
1642
+ );
1596
1643
  } else if (node.type === 'Text') {
1597
1644
  const metadata = { tracking: false, await: false };
1598
1645
  const expression = visit(node.expression, { ...state, metadata });
@@ -1684,7 +1731,12 @@ function transform_body(body, { visit, state }) {
1684
1731
  transform_children(body, { visit, state: body_state, root: true });
1685
1732
 
1686
1733
  if (body_state.update.length > 0) {
1687
- body_state.init.push(b.stmt(b.call('_$_.render', b.thunk(b.block(body_state.update)))));
1734
+ if (state.to_ts) {
1735
+ // In TypeScript mode, just add the update statements directly
1736
+ body_state.init.push(...body_state.update);
1737
+ } else {
1738
+ body_state.init.push(b.stmt(b.call('_$_.render', b.thunk(b.block(body_state.update)))));
1739
+ }
1688
1740
  }
1689
1741
 
1690
1742
  return [...body_state.setup, ...body_state.init, ...body_state.final];