ripple 0.2.91 → 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.91",
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 === '#[') {
@@ -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);
@@ -1607,7 +1629,17 @@ function transform_children(children, context) {
1607
1629
  context.state.template.push('<!>');
1608
1630
 
1609
1631
  const id = flush_node();
1610
- 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
+ );
1611
1643
  } else if (node.type === 'Text') {
1612
1644
  const metadata = { tracking: false, await: false };
1613
1645
  const expression = visit(node.expression, { ...state, metadata });
@@ -76,6 +76,19 @@ function isValidMapping(sourceContent, generatedContent) {
76
76
  if (cleanGenerated.includes(cleanSource)) return true;
77
77
  if (cleanSource.includes(cleanGenerated) && cleanGenerated.length > 2) return true;
78
78
 
79
+ // Special handling for ref callback parameters and types in createRefKey context
80
+ if (sourceContent.match(/\w+:\s*\w+/) && generatedContent.match(/\w+:\s*\w+/)) {
81
+ // This looks like a parameter with type annotation, allow mapping
82
+ return true;
83
+ }
84
+
85
+ // Allow mapping of identifiers that appear in both source and generated
86
+ if (sourceContent.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) &&
87
+ generatedContent.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/) &&
88
+ sourceContent === generatedContent) {
89
+ return true;
90
+ }
91
+
79
92
  return false;
80
93
  }
81
94
 
@@ -175,6 +188,20 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
175
188
  }
176
189
  }
177
190
 
191
+ // Special handling for type annotations in ref callbacks
192
+ if (!best_match) {
193
+ // Look for type annotations like "HTMLButtonElement"
194
+ const sourceTypeMatch = source.substring(source_offset).match(/^[A-Z][a-zA-Z0-9]*(?:Element|Type|Interface)?/);
195
+ const generatedTypeMatch = generated_code.substring(current_generated_offset).match(/^[A-Z][a-zA-Z0-9]*(?:Element|Type|Interface)?/);
196
+
197
+ if (sourceTypeMatch && generatedTypeMatch && sourceTypeMatch[0] === generatedTypeMatch[0]) {
198
+ best_match = {
199
+ source: sourceTypeMatch[0],
200
+ generated: generatedTypeMatch[0]
201
+ };
202
+ }
203
+ }
204
+
178
205
  // Handle special cases for Ripple keywords that might not have generated equivalents
179
206
  if (!best_match || best_match.source.length === 0) {
180
207
  continue;
@@ -198,10 +225,37 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
198
225
  continue;
199
226
  }
200
227
 
228
+ // Handle special ref syntax mapping for createRefKey() pattern
229
+ const sourceAtRefOffset = source.substring(Math.max(0, source_offset - 20), source_offset + 20);
230
+ const generatedAtRefOffset = generated_code.substring(Math.max(0, current_generated_offset - 20), current_generated_offset + 20);
231
+
232
+ // Check if we're dealing with ref callback syntax in source and createRefKey in generated
233
+ if (sourceAtRefOffset.includes('{ref ') && generatedAtRefOffset.includes('createRefKey')) {
234
+ // Look for the ref callback pattern in source: {ref (param: Type) => { ... }}
235
+ const refMatch = source.substring(source_offset - 50, source_offset + 50).match(/\{ref\s*\(([^)]+)\)\s*=>/);
236
+ if (refMatch) {
237
+ const paramMatch = refMatch[1].match(/(\w+):\s*(\w+)/);
238
+ if (paramMatch) {
239
+ const paramName = paramMatch[1];
240
+ const typeName = paramMatch[2];
241
+
242
+ // Map the parameter name to the generated callback parameter
243
+ if (best_match.source === paramName || best_match.source.includes(paramName)) {
244
+ // This is a ref callback parameter, allow the mapping
245
+ }
246
+ // Map the type annotation
247
+ else if (best_match.source === typeName || best_match.source.includes(typeName)) {
248
+ // This is a type annotation in ref callback, allow the mapping
249
+ }
250
+ }
251
+ }
252
+ }
253
+
201
254
  // Skip mappings for complex RefAttribute syntax to avoid overlapping sourcemaps,
202
- // but allow simple 'ref' keyword mappings for IntelliSense
203
- if (best_match.source.includes('{ref ') && best_match.source.length > 10) {
204
- // Skip complex ref expressions like '{ref (node) => { ... }}'
255
+ // but allow mappings that are part of the createRefKey pattern
256
+ if (best_match.source.includes('{ref ') && best_match.source.length > 10 &&
257
+ !generatedAtRefOffset.includes('createRefKey')) {
258
+ // Skip complex ref expressions like '{ref (node) => { ... }}' only if not using createRefKey
205
259
  continue;
206
260
  }
207
261