ripple 0.2.173 → 0.2.174

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.173",
6
+ "version": "0.2.174",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -78,9 +78,10 @@
78
78
  "devDependencies": {
79
79
  "@types/estree": "^1.0.8",
80
80
  "@types/node": "^24.3.0",
81
- "typescript": "^5.9.2"
81
+ "typescript": "^5.9.2",
82
+ "@volar/language-core": "~2.4.23"
82
83
  },
83
84
  "peerDependencies": {
84
- "ripple": "0.2.173"
85
+ "ripple": "0.2.174"
85
86
  }
86
87
  }
@@ -25,20 +25,22 @@ export function parse(source) {
25
25
  export function compile(source, filename, options = {}) {
26
26
  const ast = parse_module(source);
27
27
  const analysis = analyze(ast, filename, options);
28
- const result = options.mode === 'server'
29
- ? transform_server(filename, source, analysis)
30
- : transform_client(filename, source, analysis, false);
28
+ const result =
29
+ options.mode === 'server'
30
+ ? transform_server(filename, source, analysis)
31
+ : transform_client(filename, source, analysis, false);
31
32
 
32
33
  return result;
33
34
  }
34
35
 
35
36
  /** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
37
+ /** @import { MappingsResult } from './phases/3-transform/segments.js' */
36
38
 
37
39
  /**
38
40
  * Compile Ripple component to Volar virtual code with TypeScript mappings
39
41
  * @param {string} source
40
42
  * @param {string} filename
41
- * @returns {object} Volar mappings object
43
+ * @returns {MappingsResult} Volar mappings object
42
44
  */
43
45
  export function compile_to_volar_mappings(source, filename) {
44
46
  const ast = parse_module(source);
@@ -51,7 +53,7 @@ export function compile_to_volar_mappings(source, filename) {
51
53
  source,
52
54
  transformed.js.code,
53
55
  transformed.js.map,
54
- /** @type {PostProcessingChanges} */(transformed.js.post_processing_changes),
55
- /** @type {LineOffsets} */(transformed.js.line_offsets)
56
+ /** @type {PostProcessingChanges} */ (transformed.js.post_processing_changes),
57
+ /** @type {LineOffsets} */ (transformed.js.line_offsets),
56
58
  );
57
59
  }
@@ -1,19 +1,31 @@
1
+ /** @typedef {import('@volar/language-core').CodeMapping} VolarCodeMapping */
2
+
3
+ /**
4
+ * @typedef {Object} CustomMappingData
5
+ * @property {[number]} generatedLengths
6
+ */
7
+
8
+ /**
9
+ * @typedef {VolarCodeMapping & {
10
+ * data: VolarCodeMapping['data'] & {
11
+ * customData: CustomMappingData
12
+ * }
13
+ * }} CodeMapping
14
+ */
15
+
16
+ /** @typedef {{code: string, mappings: CodeMapping[]}} MappingsResult */
17
+
1
18
  import { walk } from 'zimmerframe';
2
19
  import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
3
20
 
21
+ /** @type {VolarCodeMapping['data']} */
4
22
  export const mapping_data = {
5
23
  verification: true,
6
24
  completion: true,
7
25
  semantic: true,
8
26
  navigation: true,
9
- rename: true,
10
- codeActions: false, // set to false to disable auto import when importing yourself
11
- formatting: false, // not doing formatting through Volar, using Prettier.
12
- // these 3 below will be true by default
13
- // leaving for reference
14
- // hover: true,
15
- // definition: true,
16
- // references: true,
27
+ structure: true,
28
+ format: false,
17
29
  };
18
30
 
19
31
  /**
@@ -28,14 +40,24 @@ export const mapping_data = {
28
40
  * @param {object} esrap_source_map - Esrap source map for accurate position lookup
29
41
  * @param {PostProcessingChanges } post_processing_changes - Optional post-processing changes
30
42
  * @param {number[]} line_offsets - Pre-computed line offsets array for generated code
31
- * @returns {{ code: string, mappings: Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}> }}
43
+ * @returns {MappingsResult}
32
44
  */
33
- export function convert_source_map_to_mappings(ast, source, generated_code, esrap_source_map, post_processing_changes, line_offsets) {
34
- /** @type {Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}>} */
45
+ export function convert_source_map_to_mappings(
46
+ ast,
47
+ source,
48
+ generated_code,
49
+ esrap_source_map,
50
+ post_processing_changes,
51
+ line_offsets,
52
+ ) {
53
+ /** @type {CodeMapping[]} */
35
54
  const mappings = [];
36
55
 
37
- // Build line offset maps for source and generated code
38
- // This allows us to convert line/column positions to byte offsets
56
+ /**
57
+ * Converts line/column positions to byte offsets
58
+ * @param {string} text
59
+ * @returns {number[]}
60
+ */
39
61
  const build_line_offsets = (text) => {
40
62
  const offsets = [0]; // Line 1 starts at offset 0
41
63
  for (let i = 0; i < text.length; i++) {
@@ -73,7 +95,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
73
95
  const adjusted_source_map = build_source_to_generated_map(
74
96
  esrap_source_map,
75
97
  post_processing_changes,
76
- line_offsets
98
+ line_offsets,
77
99
  );
78
100
 
79
101
  // Collect text tokens from AST nodes
@@ -96,11 +118,19 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
96
118
  if (node.name && node.loc) {
97
119
  // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
98
120
  if (node.metadata?.tracked_shorthand) {
99
- tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name, loc: node.loc });
121
+ tokens.push({
122
+ source: node.metadata.tracked_shorthand,
123
+ generated: node.name,
124
+ loc: node.loc,
125
+ });
100
126
  } else if (node.metadata?.is_capitalized) {
101
127
  // This identifier was capitalized during transformation
102
128
  // Map the original lowercase name to the capitalized generated name
103
- tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
129
+ tokens.push({
130
+ source: node.metadata.original_name,
131
+ generated: node.name,
132
+ loc: node.loc,
133
+ });
104
134
  } else {
105
135
  // No transformation - source and generated names are the same
106
136
  tokens.push({ source: node.name, generated: node.name, loc: node.loc });
@@ -111,7 +141,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
111
141
  // JSXIdentifiers can also be capitalized (for dynamic components)
112
142
  if (node.loc && node.name) {
113
143
  if (node.metadata?.is_capitalized) {
114
- tokens.push({ source: node.metadata.original_name, generated: node.name, loc: node.loc });
144
+ tokens.push({
145
+ source: node.metadata.original_name,
146
+ generated: node.name,
147
+ loc: node.loc,
148
+ });
115
149
  } else {
116
150
  tokens.push({ source: node.name, generated: node.name, loc: node.loc });
117
151
  }
@@ -130,7 +164,7 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
130
164
  source: '',
131
165
  generated: '',
132
166
  loc: node.loc,
133
- is_import_statement: true
167
+ is_import_statement: true,
134
168
  });
135
169
  }
136
170
 
@@ -152,7 +186,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
152
186
  visit(node.local);
153
187
  }
154
188
  return;
155
- } else if (node.type === 'ImportDefaultSpecifier' || node.type === 'ImportNamespaceSpecifier') {
189
+ } else if (
190
+ node.type === 'ImportDefaultSpecifier' ||
191
+ node.type === 'ImportNamespaceSpecifier'
192
+ ) {
156
193
  // Just visit local
157
194
  if (node.local) {
158
195
  visit(node.local);
@@ -246,17 +283,32 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
246
283
  }
247
284
 
248
285
  // 3. Push closing tag name (not visited by AST walker)
249
- if (!node.openingElement?.selfClosing && node.closingElement?.name?.type === 'JSXIdentifier') {
286
+ if (
287
+ !node.openingElement?.selfClosing &&
288
+ node.closingElement?.name?.type === 'JSXIdentifier'
289
+ ) {
250
290
  const closingNameNode = node.closingElement.name;
251
291
  if (closingNameNode.metadata?.is_capitalized) {
252
- tokens.push({ source: closingNameNode.metadata.original_name, generated: closingNameNode.name, loc: closingNameNode.loc });
292
+ tokens.push({
293
+ source: closingNameNode.metadata.original_name,
294
+ generated: closingNameNode.name,
295
+ loc: closingNameNode.loc,
296
+ });
253
297
  } else {
254
- tokens.push({ source: closingNameNode.name, generated: closingNameNode.name, loc: closingNameNode.loc });
298
+ tokens.push({
299
+ source: closingNameNode.name,
300
+ generated: closingNameNode.name,
301
+ loc: closingNameNode.loc,
302
+ });
255
303
  }
256
304
  }
257
305
 
258
306
  return;
259
- } else if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
307
+ } else if (
308
+ node.type === 'FunctionDeclaration' ||
309
+ node.type === 'FunctionExpression' ||
310
+ node.type === 'ArrowFunctionExpression'
311
+ ) {
260
312
  // Add function/component keyword token
261
313
  if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
262
314
  const source_keyword = node.metadata?.was_component ? 'component' : 'function';
@@ -266,8 +318,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
266
318
  generated: 'function',
267
319
  loc: {
268
320
  start: { line: node.loc.start.line, column: node.loc.start.column },
269
- end: { line: node.loc.start.line, column: node.loc.start.column + source_keyword.length }
270
- }
321
+ end: {
322
+ line: node.loc.start.line,
323
+ column: node.loc.start.column + source_keyword.length,
324
+ },
325
+ },
271
326
  });
272
327
  }
273
328
 
@@ -654,7 +709,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
654
709
  }
655
710
  }
656
711
  return;
657
- } else if (node.type === 'JSXClosingElement' || node.type === 'JSXClosingFragment' || node.type === 'JSXOpeningFragment') {
712
+ } else if (
713
+ node.type === 'JSXClosingElement' ||
714
+ node.type === 'JSXClosingFragment' ||
715
+ node.type === 'JSXOpeningFragment'
716
+ ) {
658
717
  // These are handled by their parent nodes
659
718
  return;
660
719
  } else if (node.type === 'JSXMemberExpression') {
@@ -736,7 +795,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
736
795
  }
737
796
  // Skip typeAnnotation
738
797
  return;
739
- } else if (node.type === 'TSTypeParameterInstantiation' || node.type === 'TSTypeParameterDeclaration') {
798
+ } else if (
799
+ node.type === 'TSTypeParameterInstantiation' ||
800
+ node.type === 'TSTypeParameterDeclaration'
801
+ ) {
740
802
  // Generic type parameters - visit to collect type variable names
741
803
  if (node.params) {
742
804
  for (const param of node.params) {
@@ -881,7 +943,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
881
943
  visit(node.typeAnnotation);
882
944
  }
883
945
  return;
884
- } else if (node.type === 'TSCallSignatureDeclaration' || node.type === 'TSConstructSignatureDeclaration') {
946
+ } else if (
947
+ node.type === 'TSCallSignatureDeclaration' ||
948
+ node.type === 'TSConstructSignatureDeclaration'
949
+ ) {
885
950
  // Call or construct signature
886
951
  if (node.typeParameters) {
887
952
  visit(node.typeParameters);
@@ -1078,7 +1143,22 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1078
1143
  visit(node.typeAnnotation);
1079
1144
  }
1080
1145
  return;
1081
- } else if (node.type === 'TSAnyKeyword' || node.type === 'TSUnknownKeyword' || node.type === 'TSNumberKeyword' || node.type === 'TSObjectKeyword' || node.type === 'TSBooleanKeyword' || node.type === 'TSBigIntKeyword' || node.type === 'TSStringKeyword' || node.type === 'TSSymbolKeyword' || node.type === 'TSVoidKeyword' || node.type === 'TSUndefinedKeyword' || node.type === 'TSNullKeyword' || node.type === 'TSNeverKeyword' || node.type === 'TSThisType' || node.type === 'TSIntrinsicKeyword') {
1146
+ } else if (
1147
+ node.type === 'TSAnyKeyword' ||
1148
+ node.type === 'TSUnknownKeyword' ||
1149
+ node.type === 'TSNumberKeyword' ||
1150
+ node.type === 'TSObjectKeyword' ||
1151
+ node.type === 'TSBooleanKeyword' ||
1152
+ node.type === 'TSBigIntKeyword' ||
1153
+ node.type === 'TSStringKeyword' ||
1154
+ node.type === 'TSSymbolKeyword' ||
1155
+ node.type === 'TSVoidKeyword' ||
1156
+ node.type === 'TSUndefinedKeyword' ||
1157
+ node.type === 'TSNullKeyword' ||
1158
+ node.type === 'TSNeverKeyword' ||
1159
+ node.type === 'TSThisType' ||
1160
+ node.type === 'TSIntrinsicKeyword'
1161
+ ) {
1082
1162
  // Primitive type keywords - leaf nodes, no children
1083
1163
  return;
1084
1164
  } else if (node.type === 'TSDeclareFunction') {
@@ -1120,23 +1200,40 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1120
1200
  }
1121
1201
 
1122
1202
  throw new Error(`Unhandled AST node type in mapping walker: ${node.type}`);
1123
- }
1203
+ },
1124
1204
  });
1125
1205
 
1126
1206
  // Process each token in order
1127
1207
  // All tokens now have .loc property - no need for fallback logic
1128
1208
  for (const token of tokens) {
1129
1209
  const source_text = token.source;
1210
+ const gen_text = token.generated;
1130
1211
 
1131
1212
  // Handle import statement full-statement mapping
1132
1213
  if (token.is_import_statement) {
1133
1214
  // Get source position from start
1134
- const source_start = loc_to_offset(token.loc.start.line, token.loc.start.column, source_line_offsets);
1135
- const source_end = loc_to_offset(token.loc.end.line, token.loc.end.column, source_line_offsets);
1215
+ const source_start = loc_to_offset(
1216
+ token.loc.start.line,
1217
+ token.loc.start.column,
1218
+ source_line_offsets,
1219
+ );
1220
+ const source_end = loc_to_offset(
1221
+ token.loc.end.line,
1222
+ token.loc.end.column,
1223
+ source_line_offsets,
1224
+ );
1136
1225
 
1137
1226
  // Get generated positions using source map
1138
- const gen_start_pos = get_generated_position(token.loc.start.line, token.loc.start.column, adjusted_source_map);
1139
- const gen_end_pos = get_generated_position(token.loc.end.line, token.loc.end.column, adjusted_source_map);
1227
+ const gen_start_pos = get_generated_position(
1228
+ token.loc.start.line,
1229
+ token.loc.start.column,
1230
+ adjusted_source_map,
1231
+ );
1232
+ const gen_end_pos = get_generated_position(
1233
+ token.loc.end.line,
1234
+ token.loc.end.column,
1235
+ adjusted_source_map,
1236
+ );
1140
1237
 
1141
1238
  if (source_start !== null && source_end !== null && gen_start_pos && gen_end_pos) {
1142
1239
  // Convert generated line:col to byte offsets
@@ -1152,7 +1249,11 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1152
1249
  lengths: [Math.min(source_length, gen_length)],
1153
1250
  data: {
1154
1251
  // only verification (diagnostics) to avoid duplicate hover/completion
1155
- verification: true
1252
+ verification: true,
1253
+
1254
+ customData: {
1255
+ generatedLengths: [gen_text.length],
1256
+ },
1156
1257
  },
1157
1258
  });
1158
1259
  }
@@ -1160,25 +1261,42 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1160
1261
  }
1161
1262
 
1162
1263
  // Use .loc to get the exact source position
1163
- const source_pos = loc_to_offset(token.loc.start.line, token.loc.start.column, source_line_offsets);
1264
+ const source_pos = loc_to_offset(
1265
+ token.loc.start.line,
1266
+ token.loc.start.column,
1267
+ source_line_offsets,
1268
+ );
1164
1269
 
1165
1270
  // Get generated position using source map
1166
- const gen_line_col = get_generated_position(token.loc.start.line, token.loc.start.column, adjusted_source_map);
1271
+ const gen_line_col = get_generated_position(
1272
+ token.loc.start.line,
1273
+ token.loc.start.column,
1274
+ adjusted_source_map,
1275
+ );
1167
1276
  let gen_pos = null;
1168
1277
  if (gen_line_col) {
1169
1278
  // Convert generated line:col to byte offset
1170
1279
  gen_pos = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
1171
1280
  } else {
1172
1281
  // No mapping found in source map - this shouldn't happen since all tokens should have mappings
1173
- console.warn(`[segments.js] No source map entry for token "${source_text}" at ${token.loc.start.line}:${token.loc.start.column}`);
1282
+ console.warn(
1283
+ `[segments.js] No source map entry for token "${source_text}" at ${token.loc.start.line}:${token.loc.start.column}`,
1284
+ );
1174
1285
  }
1175
1286
 
1176
1287
  if (source_pos !== null && gen_pos !== null) {
1288
+ // !IMPORTANT: don't set generatedLengths, otherwise Volar will use that vs our source
1289
+ // We're adding it to our custom metadata instead as we need it for patching positions
1177
1290
  mappings.push({
1178
1291
  sourceOffsets: [source_pos],
1179
1292
  generatedOffsets: [gen_pos],
1180
1293
  lengths: [source_text.length],
1181
- data: mapping_data,
1294
+ data: {
1295
+ ...mapping_data,
1296
+ customData: {
1297
+ generatedLengths: [gen_text.length],
1298
+ },
1299
+ },
1182
1300
  });
1183
1301
  }
1184
1302
  }
@@ -1195,9 +1313,10 @@ export function convert_source_map_to_mappings(ast, source, generated_code, esra
1195
1313
  lengths: [1],
1196
1314
  data: {
1197
1315
  ...mapping_data,
1198
- codeActions: true, // auto-import
1199
- rename: false, // avoid rename for a “dummy” mapping
1200
- }
1316
+ customData: {
1317
+ generatedLengths: [1],
1318
+ },
1319
+ },
1201
1320
  });
1202
1321
  }
1203
1322
 
@@ -256,10 +256,10 @@ export function bindChecked(maybe_tracked) {
256
256
  throw not_tracked_type_error('bindChecked()');
257
257
  }
258
258
 
259
- const tracked = /** @type {Tracked} */ (maybe_tracked);
259
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
260
260
 
261
261
  return (input) => {
262
- const clear_event = on(input, 'change', () => {
262
+ var clear_event = on(input, 'change', () => {
263
263
  set(tracked, input.checked);
264
264
  });
265
265
 
@@ -281,10 +281,10 @@ export function bindIndeterminate(maybe_tracked) {
281
281
  throw not_tracked_type_error('bindIndeterminate()');
282
282
  }
283
283
 
284
- const tracked = /** @type {Tracked} */ (maybe_tracked);
284
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
285
285
 
286
286
  return (input) => {
287
- const clear_event = on(input, 'change', () => {
287
+ var clear_event = on(input, 'change', () => {
288
288
  set(tracked, input.indeterminate);
289
289
  });
290
290
 
@@ -367,7 +367,7 @@ function bind_element_size(maybe_tracked, type) {
367
367
  );
368
368
 
369
369
  effect(() => {
370
- untrack(() => set(tracked, element[type]));
370
+ set(tracked, element[type]);
371
371
  return unsubscribe;
372
372
  });
373
373
  };
@@ -474,10 +474,10 @@ export function bind_content_editable(maybe_tracked, property) {
474
474
  throw not_tracked_type_error(`bind${property.charAt(0).toUpperCase() + property.slice(1)}()`);
475
475
  }
476
476
 
477
- const tracked = /** @type {Tracked} */ (maybe_tracked);
477
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
478
478
 
479
479
  return (element) => {
480
- const clear_event = on(element, 'input', () => {
480
+ var clear_event = on(element, 'input', () => {
481
481
  set(tracked, element[property]);
482
482
  });
483
483
 
@@ -533,12 +533,22 @@ export function bindFiles(maybe_tracked) {
533
533
  throw not_tracked_type_error('bindFiles()');
534
534
  }
535
535
 
536
- const tracked = /** @type {Tracked} */ (maybe_tracked);
536
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
537
537
 
538
538
  return (input) => {
539
- return on(input, 'change', () => {
539
+ var clear_event = on(input, 'change', () => {
540
540
  set(tracked, input.files);
541
541
  });
542
+
543
+ effect(() => {
544
+ var value = get(tracked);
545
+
546
+ if (value !== input.files && value instanceof FileList) {
547
+ input.files = value;
548
+ }
549
+ });
550
+
551
+ return clear_event;
542
552
  };
543
553
  }
544
554
 
@@ -552,7 +562,7 @@ export function bindNode(maybe_tracked) {
552
562
  throw not_tracked_type_error('bindNode()');
553
563
  }
554
564
 
555
- const tracked = /** @type {Tracked} */ (maybe_tracked);
565
+ var tracked = /** @type {Tracked} */ (maybe_tracked);
556
566
 
557
567
  /** @param {HTMLElement} node */
558
568
  return (node) => {
@@ -369,8 +369,10 @@ export function destroy_block(block, remove_dom = true) {
369
369
 
370
370
  if ((remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0) || (f & HEAD_BLOCK) !== 0) {
371
371
  var s = block.s;
372
- remove_block_dom(s.start, s.end);
373
- removed = true;
372
+ if (s !== null) {
373
+ remove_block_dom(s.start, s.end);
374
+ removed = true;
375
+ }
374
376
  }
375
377
 
376
378
  destroy_block_children(block, remove_dom && !removed);
@@ -284,4 +284,29 @@ describe('basic client > components & composition', () => {
284
284
  render(App);
285
285
  }).not.toThrow();
286
286
  });
287
+
288
+ it('handles component without any output', () => {
289
+ component Noop() {
290
+ // No output
291
+ }
292
+
293
+ component Op() {
294
+ <div>{'Some HTML content'}</div>
295
+ }
296
+
297
+ component App() {
298
+ let Content = track(() => Noop);
299
+ <@Content />
300
+
301
+ <button onClick={() => @Content = Op}>{'Show Op'}</button>
302
+ }
303
+
304
+ render(App);
305
+
306
+ const button = container.querySelector('button');
307
+
308
+ expect(() => {
309
+ button.click();
310
+ }).not.toThrow();
311
+ });
287
312
  });
@@ -81,7 +81,7 @@ describe('composite > props', () => {
81
81
  logs.push(@count);
82
82
  });
83
83
 
84
- <button onClick={() => @count = @count + 1}>{'+'}</button>
84
+ <button onClick={() => (@count = @count + 1)}>{'+'}</button>
85
85
  }
86
86
 
87
87
  component App() {
@@ -122,7 +122,7 @@ describe('composite > props', () => {
122
122
 
123
123
  component Toggle(props) {
124
124
  const [pressed, rest] = trackSplit(props, ['pressed']);
125
- const onClick = () => @pressed = !@pressed;
125
+ const onClick = () => (@pressed = !@pressed);
126
126
  <Button {...@rest} class={@pressed ? 'on' : 'off'} {onClick}>{'button 1'}</Button>
127
127
  <Button class={@pressed ? 'on' : 'off'} {onClick}>{'button 2'}</Button>
128
128
  }