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 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.91",
6
+ "version": "0.2.93",
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,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 to get the esrap sourcemap
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
- return convert_source_map_to_mappings(transformed.js.map, source, transformed.js.code);
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
- // 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 === '#[') {
@@ -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('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
+ ),
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
- .filter((attr) => {
1324
- if (attr.type === 'RefAttribute') {
1325
- ref_attributes.push(attr);
1326
- 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';
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
- const jsx_name = b.jsx_id(prop_name);
1349
- if (prop_name === 'children') {
1350
- has_children_props = true;
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
- // Add RefAttribute references separately for sourcemap purposes
1363
- for (const ref_attr of ref_attributes) {
1364
- const metadata = { await: false };
1365
- const argument = visit(ref_attr.argument, { ...state, metadata });
1366
- state.init.push(b.stmt(argument));
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
- const children_name = context.state.scope.generate('component');
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(b.stmt(b.call('_$_.html', id, b.thunk(expression))));
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 });