ripple 0.2.102 → 0.2.104

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.102",
6
+ "version": "0.2.104",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -64,8 +64,8 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@jridgewell/sourcemap-codec": "^1.5.5",
67
+ "@sveltejs/acorn-typescript": "^1.0.6",
67
68
  "acorn": "^8.15.0",
68
- "acorn-typescript": "^1.4.13",
69
69
  "clsx": "^2.1.1",
70
70
  "esrap": "^2.1.0",
71
71
  "is-reference": "^3.0.3",
@@ -5,12 +5,12 @@
5
5
  * } from '#compiler' */
6
6
 
7
7
  import * as acorn from 'acorn';
8
- import { tsPlugin } from 'acorn-typescript';
8
+ import { tsPlugin } from '@sveltejs/acorn-typescript';
9
9
  import { parse_style } from './style.js';
10
10
  import { walk } from 'zimmerframe';
11
11
  import { regex_newline_characters } from '../../../utils/patterns.js';
12
12
 
13
- const parser = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }), RipplePlugin());
13
+ const parser = acorn.Parser.extend(tsPlugin({ jsx: true }), RipplePlugin());
14
14
 
15
15
  /**
16
16
  * Convert JSX node types to regular JavaScript node types
@@ -67,7 +67,51 @@ function RipplePlugin(config) {
67
67
  getTokenFromCode(code) {
68
68
  if (code === 60) {
69
69
  // < character
70
- if (this.#path.findLast((n) => n.type === 'Component')) {
70
+ const inComponent = this.#path.findLast((n) => n.type === 'Component');
71
+
72
+ // Check if this could be TypeScript generics instead of JSX
73
+ // TypeScript generics appear after: identifiers, closing parens, 'new' keyword
74
+ // For example: Array<T>, func<T>(), new Map<K,V>(), method<T>()
75
+ // This check applies everywhere, not just inside components
76
+
77
+ // Look back to see what precedes the <
78
+ let lookback = this.pos - 1;
79
+
80
+ // Skip whitespace backwards
81
+ while (lookback >= 0) {
82
+ const ch = this.input.charCodeAt(lookback);
83
+ if (ch !== 32 && ch !== 9) break; // not space or tab
84
+ lookback--;
85
+ }
86
+
87
+ // Check what character/token precedes the <
88
+ if (lookback >= 0) {
89
+ const prevChar = this.input.charCodeAt(lookback);
90
+
91
+ // If preceded by identifier character (letter, digit, _, $) or closing paren,
92
+ // this is likely TypeScript generics, not JSX
93
+ const isIdentifierChar =
94
+ (prevChar >= 65 && prevChar <= 90) || // A-Z
95
+ (prevChar >= 97 && prevChar <= 122) || // a-z
96
+ (prevChar >= 48 && prevChar <= 57) || // 0-9
97
+ prevChar === 95 || // _
98
+ prevChar === 36 || // $
99
+ prevChar === 41; // )
100
+
101
+ if (isIdentifierChar) {
102
+ return super.getTokenFromCode(code);
103
+ }
104
+ }
105
+
106
+ if (inComponent) {
107
+ // Inside nested functions (scopeStack.length >= 5), treat < as relational/generic operator
108
+ // At component top-level (scopeStack.length <= 4), apply JSX detection logic
109
+ if (this.scopeStack.length >= 5) {
110
+ // Inside function - treat as TypeScript generic, not JSX
111
+ ++this.pos;
112
+ return this.finishToken(tt.relational, '<');
113
+ }
114
+
71
115
  // Check if everything before this position on the current line is whitespace
72
116
  let lineStart = this.pos - 1;
73
117
  while (
@@ -214,6 +258,88 @@ function RipplePlugin(config) {
214
258
  return node;
215
259
  }
216
260
 
261
+ /**
262
+ * Override parseSubscripts to handle `.@[expression]` syntax for reactive computed member access
263
+ * @param {any} base - The base expression
264
+ * @param {number} startPos - Start position
265
+ * @param {any} startLoc - Start location
266
+ * @param {boolean} noCalls - Whether calls are disallowed
267
+ * @param {any} maybeAsyncArrow - Optional async arrow flag
268
+ * @param {any} optionalChained - Optional chaining flag
269
+ * @param {any} forInit - For-init flag
270
+ * @returns {any} Parsed subscript expression
271
+ */
272
+ parseSubscripts(
273
+ base,
274
+ startPos,
275
+ startLoc,
276
+ noCalls,
277
+ maybeAsyncArrow,
278
+ optionalChained,
279
+ forInit,
280
+ ) {
281
+ // Check for `.@[` pattern for reactive computed member access
282
+ const isDotOrOptional = this.type === tt.dot || this.type === tt.questionDot;
283
+
284
+ if (isDotOrOptional) {
285
+ // Check the next two characters without consuming tokens
286
+ // this.pos currently points AFTER the dot token
287
+ const nextChar = this.input.charCodeAt(this.pos);
288
+ const charAfter = this.input.charCodeAt(this.pos + 1);
289
+
290
+ // Check for @[ pattern (@ = 64, [ = 91)
291
+ if (nextChar === 64 && charAfter === 91) {
292
+ const node = this.startNodeAt(startPos, startLoc);
293
+ node.object = base;
294
+ node.computed = true;
295
+ node.optional = this.type === tt.questionDot;
296
+ node.tracked = true;
297
+
298
+ // Consume the dot/questionDot token
299
+ this.next();
300
+
301
+ // Manually skip the @ character
302
+ this.pos += 1;
303
+
304
+ // Now call finishToken to properly consume the [ bracket
305
+ this.finishToken(tt.bracketL);
306
+
307
+ // Now we're positioned correctly to parse the expression
308
+ this.next(); // Move to first token inside brackets
309
+
310
+ // Parse the expression inside brackets
311
+ node.property = this.parseExpression();
312
+
313
+ // Expect closing bracket
314
+ this.expect(tt.bracketR);
315
+
316
+ // Finish this MemberExpression node
317
+ base = this.finishNode(node, 'MemberExpression');
318
+
319
+ // Recursively handle any further subscripts (chaining)
320
+ return this.parseSubscripts(
321
+ base,
322
+ startPos,
323
+ startLoc,
324
+ noCalls,
325
+ maybeAsyncArrow,
326
+ optionalChained,
327
+ forInit,
328
+ );
329
+ }
330
+ }
331
+
332
+ // Fall back to default parseSubscripts implementation
333
+ return super.parseSubscripts(
334
+ base,
335
+ startPos,
336
+ startLoc,
337
+ noCalls,
338
+ maybeAsyncArrow,
339
+ optionalChained,
340
+ forInit,
341
+ );
342
+ }
217
343
  /**
218
344
  * Parse expression atom - handles TrackedArray and TrackedObject literals
219
345
  * @param {any} [refDestructuringErrors]
@@ -1230,7 +1356,7 @@ function get_comment_handlers(source, comments, index = 0) {
1230
1356
 
1231
1357
  comments = comments
1232
1358
  .filter((comment) => comment.start >= index)
1233
- .map(({ type, value, start, end }) => ({ type, value, start, end }));
1359
+ .map(({ type, value, start, end, loc }) => ({ type, value, start, end, loc }));
1234
1360
 
1235
1361
  walk(ast, null, {
1236
1362
  _(node, { next, path }) {
@@ -1301,11 +1427,56 @@ function get_comment_handlers(source, comments, index = 0) {
1301
1427
  export function parse(source) {
1302
1428
  /** @type {CommentWithLocation[]} */
1303
1429
  const comments = [];
1304
- const { onComment, add_comments } = get_comment_handlers(source, comments);
1430
+
1431
+ // Preprocess step 1: Add trailing commas to single-parameter generics followed by (
1432
+ // This is a workaround for @sveltejs/acorn-typescript limitations with JSX enabled
1433
+ let preprocessedSource = source;
1434
+ let sourceChanged = false;
1435
+
1436
+ preprocessedSource = source.replace(/(<\s*[A-Z][a-zA-Z0-9_$]*\s*)>\s*\(/g, (_, generic) => {
1437
+ sourceChanged = true;
1438
+ // Add trailing comma to disambiguate from JSX
1439
+ return `${generic},>(`;
1440
+ });
1441
+
1442
+ // Preprocess step 2: Convert generic method shorthand in object literals to function property syntax
1443
+ // Transform `method<T,>(...): ReturnType { body }` to `method: function<T,>(...): ReturnType { body }`
1444
+ // Note: This only applies to object literal methods, not class methods
1445
+ // The trailing comma was already added by step 1
1446
+ preprocessedSource = preprocessedSource.replace(
1447
+ /(\w+)(<[A-Z][a-zA-Z0-9_$,\s]*>)\s*\(([^)]*)\)(\s*:\s*[^{]+)?(\s*\{)/g,
1448
+ (match, methodName, generics, params, returnType, brace, offset) => {
1449
+ // Look backward to determine context
1450
+ let checkPos = offset - 1;
1451
+ while (checkPos >= 0 && /\s/.test(preprocessedSource[checkPos])) checkPos--;
1452
+ const prevChar = preprocessedSource[checkPos];
1453
+
1454
+ // Check if we're inside a class
1455
+ const before = preprocessedSource.substring(Math.max(0, offset - 500), offset);
1456
+ const classMatch = before.match(/\bclass\s+\w+[^{]*\{[^}]*$/);
1457
+
1458
+ // Only transform if we're in an object literal context AND not inside a class
1459
+ if ((prevChar === '{' || prevChar === ',') && !classMatch) {
1460
+ sourceChanged = true;
1461
+ // This is object literal method shorthand - convert to function property
1462
+ // Add trailing comma if not already present
1463
+ const fixedGenerics = generics.includes(',') ? generics : generics.replace('>', ',>');
1464
+ return `${methodName}: function${fixedGenerics}(${params})${returnType || ''}${brace}`;
1465
+ }
1466
+ return match;
1467
+ },
1468
+ );
1469
+
1470
+ // Only mark as preprocessed if we actually changed something
1471
+ if (!sourceChanged) {
1472
+ preprocessedSource = source;
1473
+ }
1474
+
1475
+ const { onComment, add_comments } = get_comment_handlers(preprocessedSource, comments);
1305
1476
  let ast;
1306
1477
 
1307
1478
  try {
1308
- ast = parser.parse(source, {
1479
+ ast = parser.parse(preprocessedSource, {
1309
1480
  sourceType: 'module',
1310
1481
  ecmaVersion: 13,
1311
1482
  locations: true,
@@ -106,7 +106,7 @@ const visitors = {
106
106
  }
107
107
 
108
108
  if (
109
- is_reference(node, /** @type {Node} */ (parent)) &&
109
+ is_reference(node, /** @type {Node} */(parent)) &&
110
110
  node.tracked &&
111
111
  binding?.node !== node
112
112
  ) {
@@ -117,7 +117,7 @@ const visitors = {
117
117
  }
118
118
 
119
119
  if (
120
- is_reference(node, /** @type {Node} */ (parent)) &&
120
+ is_reference(node, /** @type {Node} */(parent)) &&
121
121
  node.tracked &&
122
122
  binding?.node !== node
123
123
  ) {
@@ -250,7 +250,10 @@ const visitors = {
250
250
  }
251
251
  const elements = [];
252
252
 
253
- context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1 });
253
+ // Track metadata for this component
254
+ const metadata = { await: false };
255
+
256
+ context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1, metadata });
254
257
 
255
258
  const css = node.css;
256
259
 
@@ -259,6 +262,12 @@ const visitors = {
259
262
  prune_css(css, node);
260
263
  }
261
264
  }
265
+
266
+ // Store component metadata in analysis
267
+ context.state.analysis.component_metadata.push({
268
+ id: node.id.name,
269
+ async: metadata.await,
270
+ });
262
271
  },
263
272
 
264
273
  ForStatement(node, context) {
@@ -357,6 +366,11 @@ const visitors = {
357
366
  }
358
367
 
359
368
  if (node.pending) {
369
+ // Try/pending blocks indicate async operations
370
+ if (context.state.metadata?.await === false) {
371
+ context.state.metadata.await = true;
372
+ }
373
+
360
374
  node.metadata = {
361
375
  has_template: false,
362
376
  };
@@ -594,6 +608,7 @@ export function analyze(ast, filename) {
594
608
  ast,
595
609
  scope,
596
610
  scopes,
611
+ component_metadata: [],
597
612
  };
598
613
 
599
614
  walk(
@@ -1,4 +1,4 @@
1
- /** @import {Expression, FunctionExpression} from 'estree' */
1
+ /** @import {Expression, FunctionExpression, Pattern} from 'estree' */
2
2
 
3
3
  import { walk } from 'zimmerframe';
4
4
  import path from 'node:path';
@@ -293,7 +293,6 @@ const visitors = {
293
293
 
294
294
  NewExpression(node, context) {
295
295
  const callee = node.callee;
296
- const parent = context.path.at(-1);
297
296
 
298
297
  if (context.state.metadata?.tracking === false) {
299
298
  context.state.metadata.tracking = true;
@@ -305,18 +304,22 @@ const visitors = {
305
304
  is_inside_call_expression(context) ||
306
305
  is_value_static(node)
307
306
  ) {
307
+ if (!context.state.to_ts) {
308
+ delete node.typeArguments;
309
+ }
308
310
  return context.next();
309
311
  }
310
312
 
311
- return b.call(
312
- '_$_.with_scope',
313
- b.id('__block'),
314
- b.thunk({
315
- ...node,
316
- callee: context.visit(callee),
317
- arguments: node.arguments.map((arg) => context.visit(arg)),
318
- }),
319
- );
313
+ const new_node = {
314
+ ...node,
315
+ callee: context.visit(callee),
316
+ arguments: node.arguments.map((arg) => context.visit(arg)),
317
+ };
318
+ if (!context.state.to_ts) {
319
+ delete new_node.typeArguments;
320
+ }
321
+
322
+ return b.call('_$_.with_scope', b.id('__block'), b.thunk(new_node));
320
323
  },
321
324
 
322
325
  TrackedArrayExpression(node, context) {
@@ -366,7 +369,7 @@ const visitors = {
366
369
  context.state.metadata.tracking = true;
367
370
  }
368
371
 
369
- if (node.property.type === 'Identifier' && node.property.tracked) {
372
+ if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
370
373
  add_ripple_internal_import(context);
371
374
 
372
375
  return b.call(
@@ -918,8 +921,7 @@ const visitors = {
918
921
 
919
922
  if (
920
923
  left.type === 'MemberExpression' &&
921
- left.property.type === 'Identifier' &&
922
- left.property.tracked
924
+ (left.tracked || (left.property.type === 'Identifier' && left.property.tracked))
923
925
  ) {
924
926
  add_ripple_internal_import(context);
925
927
  const operator = node.operator;
@@ -976,8 +978,7 @@ const visitors = {
976
978
 
977
979
  if (
978
980
  argument.type === 'MemberExpression' &&
979
- argument.property.type === 'Identifier' &&
980
- argument.property.tracked
981
+ (argument.tracked || (argument.property.type === 'Identifier' && argument.property.tracked))
981
982
  ) {
982
983
  add_ripple_internal_import(context);
983
984
  context.state.metadata.tracking = true;
@@ -1052,7 +1053,9 @@ const visitors = {
1052
1053
  ),
1053
1054
  ),
1054
1055
  b.literal(flags),
1055
- key != null ? b.arrow(index ? [pattern, index] : [pattern], context.visit(key)) : undefined,
1056
+ key != null
1057
+ ? b.arrow(index ? [pattern, index] : [pattern], context.visit(key))
1058
+ : undefined,
1056
1059
  ),
1057
1060
  ),
1058
1061
  );
@@ -1401,6 +1404,9 @@ function transform_ts_child(node, context) {
1401
1404
  ...context,
1402
1405
  state: { ...context.state, scope: body_scope },
1403
1406
  });
1407
+ if (node.key) {
1408
+ block_body.unshift(b.stmt(visit(node.key)));
1409
+ }
1404
1410
  if (node.index) {
1405
1411
  block_body.unshift(b.let(visit(node.index), b.literal(0)));
1406
1412
  }