ripple 0.2.68 → 0.2.70

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.68",
6
+ "version": "0.2.70",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -192,6 +192,135 @@ function RipplePlugin(config) {
192
192
  return super.parseExportDefaultDeclaration();
193
193
  }
194
194
 
195
+ parseForStatement(node) {
196
+ this.next()
197
+ let awaitAt = (this.options.ecmaVersion >= 9 && this.canAwait && this.eatContextual("await")) ? this.lastTokStart : -1
198
+ this.labels.push({kind: "loop"})
199
+ this.enterScope(0)
200
+ this.expect(tt.parenL)
201
+
202
+ if (this.type === tt.semi) {
203
+ if (awaitAt > -1) this.unexpected(awaitAt)
204
+ return this.parseFor(node, null)
205
+ }
206
+
207
+ let isLet = this.isLet()
208
+ if (this.type === tt._var || this.type === tt._const || isLet) {
209
+ let init = this.startNode(), kind = isLet ? "let" : this.value
210
+ this.next()
211
+ this.parseVar(init, true, kind)
212
+ this.finishNode(init, "VariableDeclaration")
213
+ return this.parseForAfterInitWithIndex(node, init, awaitAt)
214
+ }
215
+
216
+ // Handle other cases like using declarations if they exist
217
+ let startsWithLet = this.isContextual("let"), isForOf = false
218
+ let usingKind = (this.isUsing && this.isUsing(true)) ? "using" : (this.isAwaitUsing && this.isAwaitUsing(true)) ? "await using" : null
219
+ if (usingKind) {
220
+ let init = this.startNode()
221
+ this.next()
222
+ if (usingKind === "await using") {
223
+ if (!this.canAwait) {
224
+ this.raise(this.start, "Await using cannot appear outside of async function")
225
+ }
226
+ this.next()
227
+ }
228
+ this.parseVar(init, true, usingKind)
229
+ this.finishNode(init, "VariableDeclaration")
230
+ return this.parseForAfterInitWithIndex(node, init, awaitAt)
231
+ }
232
+
233
+ let containsEsc = this.containsEsc
234
+ let refDestructuringErrors = {}
235
+ let initPos = this.start
236
+ let init = awaitAt > -1
237
+ ? this.parseExprSubscripts(refDestructuringErrors, "await")
238
+ : this.parseExpression(true, refDestructuringErrors)
239
+
240
+ if (this.type === tt._in || (isForOf = this.options.ecmaVersion >= 6 && this.isContextual("of"))) {
241
+ if (awaitAt > -1) { // implies `ecmaVersion >= 9`
242
+ if (this.type === tt._in) this.unexpected(awaitAt)
243
+ node.await = true
244
+ } else if (isForOf && this.options.ecmaVersion >= 8) {
245
+ if (init.start === initPos && !containsEsc && init.type === "Identifier" && init.name === "async") this.unexpected()
246
+ else if (this.options.ecmaVersion >= 9) node.await = false
247
+ }
248
+ if (startsWithLet && isForOf) this.raise(init.start, "The left-hand side of a for-of loop may not start with 'let'.")
249
+ this.toAssignable(init, false, refDestructuringErrors)
250
+ this.checkLValPattern(init)
251
+ return this.parseForInWithIndex(node, init)
252
+ } else {
253
+ this.checkExpressionErrors(refDestructuringErrors, true)
254
+ }
255
+
256
+ if (awaitAt > -1) this.unexpected(awaitAt)
257
+ return this.parseFor(node, init)
258
+ }
259
+
260
+ parseForAfterInitWithIndex(node, init, awaitAt) {
261
+ if ((this.type === tt._in || (this.options.ecmaVersion >= 6 && this.isContextual("of"))) && init.declarations.length === 1) {
262
+ if (this.options.ecmaVersion >= 9) {
263
+ if (this.type === tt._in) {
264
+ if (awaitAt > -1) this.unexpected(awaitAt)
265
+ } else node.await = awaitAt > -1
266
+ }
267
+ return this.parseForInWithIndex(node, init)
268
+ }
269
+ if (awaitAt > -1) this.unexpected(awaitAt)
270
+ return this.parseFor(node, init)
271
+ }
272
+
273
+ parseForInWithIndex(node, init) {
274
+ const isForIn = this.type === tt._in
275
+ this.next()
276
+
277
+ if (
278
+ init.type === "VariableDeclaration" &&
279
+ init.declarations[0].init != null &&
280
+ (
281
+ !isForIn ||
282
+ this.options.ecmaVersion < 8 ||
283
+ this.strict ||
284
+ init.kind !== "var" ||
285
+ init.declarations[0].id.type !== "Identifier"
286
+ )
287
+ ) {
288
+ this.raise(
289
+ init.start,
290
+ `${isForIn ? "for-in" : "for-of"} loop variable declaration may not have an initializer`
291
+ )
292
+ }
293
+
294
+ node.left = init
295
+ node.right = isForIn ? this.parseExpression() : this.parseMaybeAssign()
296
+
297
+ // Check for our extended syntax: "; index varName"
298
+ if (!isForIn && this.type === tt.semi) {
299
+ this.next() // consume ';'
300
+
301
+ if (this.isContextual('index')) {
302
+ this.next() // consume 'index'
303
+
304
+ if (this.type === tt.name) {
305
+ node.index = this.parseIdent()
306
+ } else {
307
+ this.raise(this.start, 'Expected identifier after "index" keyword')
308
+ }
309
+ } else {
310
+ this.raise(this.start, 'Expected "index" keyword after semicolon in for-of loop')
311
+ }
312
+ } else if (!isForIn) {
313
+ // Set index to null for standard for-of loops
314
+ node.index = null
315
+ }
316
+
317
+ this.expect(tt.parenR)
318
+ node.body = this.parseStatement("for")
319
+ this.exitScope()
320
+ this.labels.pop()
321
+ return this.finishNode(node, isForIn ? "ForInStatement" : "ForOfStatement")
322
+ }
323
+
195
324
  shouldParseExportStatement() {
196
325
  if (super.shouldParseExportStatement()) {
197
326
  return true;
@@ -791,6 +920,8 @@ function RipplePlugin(config) {
791
920
  return node;
792
921
  }
793
922
 
923
+
924
+
794
925
  if (this.type.label === '@') {
795
926
  // Try to parse as an expression statement first using tryParse
796
927
  // This allows us to handle Ripple @ syntax like @count++ without
@@ -5,7 +5,7 @@ import {
5
5
  get_delegated_event,
6
6
  is_element_dom_element,
7
7
  is_inside_component,
8
- is_ripple_import,
8
+ is_ripple_track_call,
9
9
  is_void_element,
10
10
  } from '../../utils.js';
11
11
  import { extract_paths } from '../../../utils/ast.js';
@@ -139,16 +139,7 @@ const visitors = {
139
139
  CallExpression(node, context) {
140
140
  const callee = node.callee;
141
141
 
142
- if (
143
- context.state.function_depth === 0 &&
144
- ((callee.type === 'Identifier' && callee.name === 'track') ||
145
- (callee.type === 'MemberExpression' &&
146
- callee.object.type === 'Identifier' &&
147
- callee.property.type === 'Identifier' &&
148
- callee.property.name === 'track' &&
149
- !callee.computed)) &&
150
- is_ripple_import(callee, context)
151
- ) {
142
+ if (context.state.function_depth === 0 && is_ripple_track_call(callee, context)) {
152
143
  error(
153
144
  '`track` can only be used within a reactive context, such as a component, function or class that is used or created from a component',
154
145
  context.state.analysis.module.filename,
@@ -278,6 +269,21 @@ const visitors = {
278
269
  return context.next();
279
270
  }
280
271
 
272
+ if (node.index) {
273
+ const state = context.state;
274
+ const scope = state.scopes.get(node);
275
+ const binding = scope.get(node.index.name);
276
+ binding.kind = 'index'
277
+
278
+ if (binding !== null) {
279
+ binding.transform = {
280
+ read: (node) => {
281
+ return b.call('_$_.get', node)
282
+ },
283
+ };
284
+ }
285
+ }
286
+
281
287
  node.metadata = {
282
288
  has_template: false,
283
289
  };
@@ -3,7 +3,13 @@ import path from 'node:path';
3
3
  import { print } from 'esrap';
4
4
  import tsx from 'esrap/languages/tsx';
5
5
  import * as b from '../../../utils/builders.js';
6
- import { IS_CONTROLLED, TEMPLATE_FRAGMENT } from '../../../constants.js';
6
+ import {
7
+ IS_CONTROLLED,
8
+ IS_INDEXED,
9
+ TEMPLATE_FRAGMENT,
10
+ TEMPLATE_SVG_NAMESPACE,
11
+ TEMPLATE_MATHML_NAMESPACE,
12
+ } from '../../../constants.js';
7
13
  import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
8
14
  import {
9
15
  build_hoisted_params,
@@ -13,7 +19,6 @@ import {
13
19
  escape_html,
14
20
  is_boolean_attribute,
15
21
  is_dom_property,
16
- is_ripple_import,
17
22
  is_declared_function_within_component,
18
23
  is_inside_call_expression,
19
24
  is_value_static,
@@ -21,6 +26,7 @@ import {
21
26
  is_component_level_function,
22
27
  is_element_dom_element,
23
28
  is_top_level_await,
29
+ is_ripple_track_call,
24
30
  } from '../../utils.js';
25
31
  import is_reference from 'is-reference';
26
32
  import { object } from '../../../utils/ast.js';
@@ -104,6 +110,22 @@ function build_getter(node, context) {
104
110
  return node;
105
111
  }
106
112
 
113
+ function determine_namespace_for_children(element_name, current_namespace) {
114
+ if (element_name === 'foreignObject') {
115
+ return 'html';
116
+ }
117
+
118
+ if (element_name === 'svg') {
119
+ return 'svg';
120
+ }
121
+
122
+ if (element_name === 'math') {
123
+ return 'mathml';
124
+ }
125
+
126
+ return current_namespace;
127
+ }
128
+
107
129
  const visitors = {
108
130
  _: function set_scope(node, { next, state }) {
109
131
  const scope = state.scopes.get(node);
@@ -128,7 +150,10 @@ const visitors = {
128
150
  if (
129
151
  (context.state.metadata?.tracking === false ||
130
152
  (parent.type !== 'AssignmentExpression' && parent.type !== 'UpdateExpression')) &&
131
- (node.tracked || binding?.kind === 'prop' || binding?.kind === 'prop_fallback') &&
153
+ (node.tracked ||
154
+ binding?.kind === 'prop' ||
155
+ binding?.kind === 'index' ||
156
+ binding?.kind === 'prop_fallback') &&
132
157
  binding?.node !== node
133
158
  ) {
134
159
  if (context.state.metadata?.tracking === false) {
@@ -165,16 +190,7 @@ const visitors = {
165
190
  context.state.metadata.tracking = true;
166
191
  }
167
192
 
168
- if (
169
- !context.state.to_ts &&
170
- ((callee.type === 'Identifier' && callee.name === 'track') ||
171
- (callee.type === 'MemberExpression' &&
172
- callee.object.type === 'Identifier' &&
173
- callee.property.type === 'Identifier' &&
174
- callee.property.name === 'track' &&
175
- !callee.computed)) &&
176
- is_ripple_import(callee, context)
177
- ) {
193
+ if (!context.state.to_ts && is_ripple_track_call(callee, context)) {
178
194
  if (node.arguments.length === 0) {
179
195
  node.arguments.push(b.void0);
180
196
  }
@@ -427,6 +443,10 @@ const visitors = {
427
443
  const is_spreading = node.attributes.some((attr) => attr.type === 'SpreadAttribute');
428
444
  const spread_attributes = is_spreading ? [] : null;
429
445
 
446
+ const child_namespace = is_dom_element
447
+ ? determine_namespace_for_children(node.id.name, state.namespace)
448
+ : state.namespace;
449
+
430
450
  const handle_static_attr = (name, value) => {
431
451
  const attr_value = b.literal(
432
452
  ` ${name}${
@@ -629,12 +649,16 @@ const visitors = {
629
649
  if (node.metadata.scoped && state.component.css) {
630
650
  expression = b.binary('+', b.literal(state.component.css.hash + ' '), expression);
631
651
  }
632
- const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg'
652
+ const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg';
633
653
 
634
- if (metadata.tracking) {
635
- local_updates.push(b.stmt(b.call('_$_.set_class', id, expression, is_html)));
654
+ if (class_attribute.name.name === '$class' || metadata.tracking) {
655
+ local_updates.push(
656
+ b.stmt(b.call('_$_.set_class', id, expression, undefined, b.literal(is_html))),
657
+ );
636
658
  } else {
637
- state.init.push(b.stmt(b.call('_$_.set_class', id, expression, is_html)));
659
+ state.init.push(
660
+ b.stmt(b.call('_$_.set_class', id, expression, undefined, b.literal(is_html))),
661
+ );
638
662
  }
639
663
  }
640
664
  } else if (node.metadata.scoped && state.component.css) {
@@ -658,7 +682,7 @@ const visitors = {
658
682
  if (!is_void) {
659
683
  transform_children(node.children, {
660
684
  visit,
661
- state: { ...state, init, update },
685
+ state: { ...state, init, update, namespace: child_namespace },
662
686
  root: false,
663
687
  });
664
688
  state.template.push(`</${node.id.name}>`);
@@ -769,7 +793,7 @@ const visitors = {
769
793
  for (const child of node.children) {
770
794
  if (child.type === 'Component') {
771
795
  const id = child.id;
772
- props.push(b.prop('init', id, visit(child, state)));
796
+ props.push(b.prop('init', id, visit(child, { ...state, namespace: child_namespace })));
773
797
  } else {
774
798
  children_filtered.push(child);
775
799
  }
@@ -780,6 +804,7 @@ const visitors = {
780
804
  const children = visit(b.component(b.id('children'), [], children_filtered), {
781
805
  ...context.state,
782
806
  scope: component_scope,
807
+ namespace: child_namespace,
783
808
  });
784
809
 
785
810
  if (children_prop) {
@@ -1001,6 +1026,12 @@ const visitors = {
1001
1026
  return;
1002
1027
  }
1003
1028
  const is_controlled = node.is_controlled;
1029
+ const index = node.index;
1030
+ let flags = is_controlled ? IS_CONTROLLED : 0;
1031
+
1032
+ if (index !== null) {
1033
+ flags |= IS_INDEXED;
1034
+ }
1004
1035
 
1005
1036
  // do only if not controller
1006
1037
  if (!is_controlled) {
@@ -1018,15 +1049,15 @@ const visitors = {
1018
1049
  id,
1019
1050
  b.thunk(context.visit(node.right)),
1020
1051
  b.arrow(
1021
- [b.id('__anchor'), pattern],
1052
+ index ? [b.id('__anchor'), pattern, index] : [b.id('__anchor'), pattern],
1022
1053
  b.block(
1023
1054
  transform_body(node.body.body, {
1024
1055
  ...context,
1025
- state: { ...context.state, scope: body_scope },
1056
+ state: { ...context.state, scope: body_scope, namespace: context.state.namespace },
1026
1057
  }),
1027
1058
  ),
1028
1059
  ),
1029
- b.literal(is_controlled ? IS_CONTROLLED : 0),
1060
+ b.literal(flags),
1030
1061
  ),
1031
1062
  ),
1032
1063
  );
@@ -1470,9 +1501,15 @@ function transform_children(children, context) {
1470
1501
 
1471
1502
  if (child.type === 'Text' && prev_child?.type === 'Text') {
1472
1503
  if (child.expression.type === 'Literal' && prev_child.expression.type === 'Literal') {
1473
- prev_child.expression = b.literal(prev_child.expression.value + child.expression.value);
1504
+ prev_child.expression = b.literal(
1505
+ prev_child.expression.value + String(child.expression.value),
1506
+ );
1474
1507
  } else {
1475
- prev_child.expression = b.binary('+', prev_child.expression, child.expression);
1508
+ prev_child.expression = b.binary(
1509
+ '+',
1510
+ prev_child.expression,
1511
+ b.call('String', child.expression),
1512
+ );
1476
1513
  }
1477
1514
  normalized.splice(i, 1);
1478
1515
  }
@@ -1540,7 +1577,7 @@ function transform_children(children, context) {
1540
1577
  prev = flush_node;
1541
1578
 
1542
1579
  if (node.type === 'Element') {
1543
- visit(node, { ...state, flush_node });
1580
+ visit(node, { ...state, flush_node, namespace: state.namespace });
1544
1581
  } else if (node.type === 'Text') {
1545
1582
  const metadata = { tracking: false, await: false };
1546
1583
  const expression = visit(node.expression, { ...state, metadata });
@@ -1556,13 +1593,10 @@ function transform_children(children, context) {
1556
1593
  if (expression.type === 'Literal') {
1557
1594
  state.template.push(escape_html(expression.value));
1558
1595
  } else {
1559
- const id = state.flush_node();
1596
+ const id = flush_node();
1560
1597
  state.template.push(' ');
1561
- state.init.push(
1562
- b.stmt(
1563
- b.assignment('=', b.member(b.call('_$_.child', id), b.id('nodeValue')), expression),
1564
- ),
1565
- );
1598
+ // avoid set_text overhead for single text nodes
1599
+ state.init.push(b.stmt(b.assignment('=', b.member(id, b.id('nodeValue')), expression)));
1566
1600
  }
1567
1601
  } else {
1568
1602
  // Handle Text nodes in fragments
@@ -1576,26 +1610,33 @@ function transform_children(children, context) {
1576
1610
  } else if (node.type === 'ForOfStatement') {
1577
1611
  const is_controlled = normalized.length === 1;
1578
1612
  node.is_controlled = is_controlled;
1579
- visit(node, { ...state, flush_node });
1613
+ visit(node, { ...state, flush_node, namespace: state.namespace });
1580
1614
  } else if (node.type === 'IfStatement') {
1581
1615
  const is_controlled = normalized.length === 1;
1582
1616
  node.is_controlled = is_controlled;
1583
- visit(node, { ...state, flush_node });
1617
+ visit(node, { ...state, flush_node, namespace: state.namespace });
1584
1618
  } else if (node.type === 'TryStatement') {
1585
1619
  const is_controlled = normalized.length === 1;
1586
1620
  node.is_controlled = is_controlled;
1587
- visit(node, { ...state, flush_node });
1621
+ visit(node, { ...state, flush_node, namespace: state.namespace });
1588
1622
  } else {
1589
1623
  debugger;
1590
1624
  }
1591
1625
  }
1592
1626
  }
1593
1627
 
1628
+ const template_namespace = state.namespace || 'html';
1629
+
1594
1630
  if (root && initial !== null && template_id !== null) {
1595
- const flags = is_fragment ? b.literal(TEMPLATE_FRAGMENT) : b.literal(0);
1631
+ let flags = is_fragment ? TEMPLATE_FRAGMENT : 0;
1632
+ if (template_namespace === 'svg') {
1633
+ flags |= TEMPLATE_SVG_NAMESPACE;
1634
+ } else if (template_namespace === 'mathml') {
1635
+ flags |= TEMPLATE_MATHML_NAMESPACE;
1636
+ }
1596
1637
  state.final.push(b.stmt(b.call('_$_.append', b.id('__anchor'), initial)));
1597
1638
  state.hoisted.push(
1598
- b.var(template_id, b.call('_$_.template', join_template(state.template), flags)),
1639
+ b.var(template_id, b.call('_$_.template', join_template(state.template), b.literal(flags))),
1599
1640
  );
1600
1641
  }
1601
1642
  }
@@ -1609,6 +1650,7 @@ function transform_body(body, { visit, state }) {
1609
1650
  update: [],
1610
1651
  final: [],
1611
1652
  metadata: state.metadata,
1653
+ namespace: state.namespace || 'html', // Preserve namespace context
1612
1654
  };
1613
1655
 
1614
1656
  transform_children(body, { visit, state: body_state, root: true });
@@ -1637,7 +1679,9 @@ export function transform(filename, source, analysis, to_ts) {
1637
1679
  to_ts,
1638
1680
  };
1639
1681
 
1640
- const program = /** @type {ESTree.Program} */ (walk(analysis.ast, state, visitors));
1682
+ const program = /** @type {ESTree.Program} */ (
1683
+ walk(analysis.ast, { ...state, namespace: 'html' }, visitors)
1684
+ );
1641
1685
 
1642
1686
  for (const hoisted of state.hoisted) {
1643
1687
  program.body.unshift(hoisted);