ripple 0.2.146 → 0.2.148

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.146",
6
+ "version": "0.2.148",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -81,6 +81,6 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "ripple": "0.2.146"
84
+ "ripple": "0.2.148"
85
85
  }
86
86
  }
@@ -976,6 +976,15 @@ function RipplePlugin(config) {
976
976
  return this.finishNode(node, 'JSXExpressionContainer');
977
977
  }
978
978
 
979
+ jsx_parseEmptyExpression() {
980
+ // Override to properly handle the range for JSXEmptyExpression
981
+ // The range should be from after { to before }
982
+ const node = this.startNodeAt(this.lastTokEnd, this.lastTokEndLoc);
983
+ node.end = this.start;
984
+ node.loc.end = this.startLoc;
985
+ return this.finishNodeAt(node, 'JSXEmptyExpression', this.start, this.startLoc);
986
+ }
987
+
979
988
  jsx_parseTupleContainer() {
980
989
  var t = this.startNode();
981
990
  return (
@@ -1588,9 +1597,7 @@ function RipplePlugin(config) {
1588
1597
  const node = this.jsx_parseExpressionContainer();
1589
1598
  node.type = node.html ? 'Html' : 'Text';
1590
1599
  delete node.html;
1591
- if (node.expression.type !== 'JSXEmptyExpression') {
1592
- body.push(node);
1593
- }
1600
+ body.push(node);
1594
1601
  } else if (this.type.label === '}') {
1595
1602
  return;
1596
1603
  } else if (this.type.label === 'jsxTagStart') {
@@ -1915,6 +1922,17 @@ function get_comment_handlers(source, comments, index = 0) {
1915
1922
  return;
1916
1923
  }
1917
1924
  }
1925
+ // Handle JSXEmptyExpression - these represent {/* comment */} in JSX
1926
+ if (node.type === 'JSXEmptyExpression') {
1927
+ // Collect all comments that fall within this JSXEmptyExpression
1928
+ while (comments[0] && comments[0].start >= node.start && comments[0].end <= node.end) {
1929
+ comment = /** @type {CommentWithLocation} */ (comments.shift());
1930
+ (node.innerComments ||= []).push(comment);
1931
+ }
1932
+ if (node.innerComments && node.innerComments.length > 0) {
1933
+ return;
1934
+ }
1935
+ }
1918
1936
  // Handle empty Element nodes the same way as empty BlockStatements
1919
1937
  if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
1920
1938
  if (comments[0].start < node.end && comments[0].end < node.end) {
@@ -45,8 +45,6 @@ function mark_control_flow_has_template(path) {
45
45
 
46
46
  function visit_function(node, context) {
47
47
  node.metadata = {
48
- hoisted: false,
49
- hoisted_params: [],
50
48
  scope: context.state.scope,
51
49
  tracked: false,
52
50
  };
@@ -722,12 +720,7 @@ const visitors = {
722
720
  const handler = visit(attr.value, state);
723
721
  const delegated_event = get_delegated_event(event_name, handler, state);
724
722
 
725
- if (delegated_event !== null) {
726
- if (delegated_event.hoisted) {
727
- delegated_event.function.metadata.hoisted = true;
728
- delegated_event.hoisted = true;
729
- }
730
-
723
+ if (delegated_event) {
731
724
  if (attr.metadata === undefined) {
732
725
  attr.metadata = {};
733
726
  }
@@ -17,7 +17,6 @@ import {
17
17
  } from '../../../../constants.js';
18
18
  import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
19
19
  import {
20
- build_hoisted_params,
21
20
  is_inside_component,
22
21
  build_assignment,
23
22
  visit_assignment_expression,
@@ -75,16 +74,6 @@ function visit_function(node, context) {
75
74
  }
76
75
  }
77
76
 
78
- if (metadata?.hoisted === true) {
79
- const params = build_hoisted_params(node, context);
80
-
81
- return /** @type {FunctionExpression} */ ({
82
- ...node,
83
- params,
84
- body: context.visit(node.body, state),
85
- });
86
- }
87
-
88
77
  let body = context.visit(node.body, {
89
78
  ...state,
90
79
  // we are new context so tracking no longer applies
@@ -132,13 +121,7 @@ function visit_head_element(node, context) {
132
121
  '_$_.head',
133
122
  b.arrow(
134
123
  [b.id('__anchor')],
135
- b.block([
136
- ...init,
137
- ...update.map((u) => {
138
- debugger;
139
- }),
140
- ...final,
141
- ]),
124
+ b.block([...init, ...update.map((u) => u.operation), ...final]),
142
125
  ),
143
126
  ),
144
127
  );
@@ -985,21 +968,7 @@ const visitors = {
985
968
  state.events.add(event_name);
986
969
  }
987
970
 
988
- // Hoist function if we can, otherwise we leave the function as is
989
- if (attr.metadata.delegated.hoisted) {
990
- if (attr.metadata.delegated.function === attr.value) {
991
- const func_name = state.scope.root.unique('on_' + event_name);
992
- state.hoisted.push(b.var(func_name, handler));
993
- handler = func_name;
994
- }
995
-
996
- const hoisted_params = /** @type {Expression[]} */ (
997
- attr.metadata.delegated.function.metadata.hoisted_params
998
- );
999
-
1000
- const args = [handler, b.id('__block'), ...hoisted_params];
1001
- delegated_assignment = b.array(args);
1002
- } else if (
971
+ if (
1003
972
  (handler.type === 'Identifier' &&
1004
973
  is_declared_function_within_component(handler, context)) ||
1005
974
  handler.type === 'ArrowFunctionExpression' ||
@@ -2125,7 +2094,6 @@ function transform_ts_child(node, context) {
2125
2094
  .map((child) => visit(child, state))
2126
2095
  .filter((child) => child.type !== 'JSXText' || child.value.trim() !== '');
2127
2096
 
2128
- debugger;
2129
2097
  state.init.push(b.stmt(b.jsx_fragment(children)));
2130
2098
  } else {
2131
2099
  throw new Error('TODO');
@@ -323,8 +323,5 @@ export interface TransformContext {
323
323
  * Delegated event result
324
324
  */
325
325
  export interface DelegatedEventResult {
326
- /** Whether event was hoisted */
327
- hoisted: boolean;
328
- /** The hoisted function */
329
326
  function?: FunctionExpression | FunctionDeclaration | ArrowFunctionExpression;
330
327
  }
@@ -169,206 +169,19 @@ export function is_dom_property(name) {
169
169
  return DOM_PROPERTIES.includes(name);
170
170
  }
171
171
 
172
- const unhoisted = { hoisted: false };
173
-
174
172
  /**
175
- * Determines if an event handler can be hoisted for delegation
173
+ * Determines if an event handler can be delegated
176
174
  * @param {string} event_name
177
175
  * @param {Expression} handler
178
176
  * @param {CompilerState} state
179
- * @returns {DelegatedEventResult | null}
177
+ * @returns {boolean}
180
178
  */
181
179
  export function get_delegated_event(event_name, handler, state) {
182
180
  // Handle delegated event handlers. Bail out if not a delegated event.
183
181
  if (!handler || !is_delegated(event_name)) {
184
- return null;
185
- }
186
-
187
- /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
188
- let target_function = null;
189
- let binding = null;
190
-
191
- if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
192
- target_function = handler;
193
- } else if (handler.type === 'Identifier') {
194
- binding = state.scope.get(handler.name);
195
-
196
- if (state.analysis.module.scope.references.has(handler.name)) {
197
- // If a binding with the same name is referenced in the module scope (even if not declared there), bail out
198
- return unhoisted;
199
- }
200
-
201
- if (binding != null) {
202
- for (const { path } of binding.references) {
203
- const parent = path.at(-1);
204
- if (parent === undefined) {
205
- return unhoisted;
206
- }
207
-
208
- const grandparent = path.at(-2);
209
-
210
- /** @type {Element | null} */
211
- let element = null;
212
- /** @type {string | null} */
213
- let event_name = null;
214
- if (
215
- parent.type === 'Expression' &&
216
- grandparent?.type === 'Attribute' &&
217
- is_event_attribute(grandparent)
218
- ) {
219
- element = /** @type {Element} */ (path.at(-3));
220
- const attribute = /** @type {Attribute} */ (grandparent);
221
- event_name = get_attribute_event_name(attribute.name.name);
222
- }
223
-
224
- if (element && event_name) {
225
- if (
226
- element.type !== 'Element' ||
227
- element.metadata.has_spread ||
228
- !is_delegated(event_name)
229
- ) {
230
- return unhoisted;
231
- }
232
- } else if (
233
- parent.type !== 'FunctionDeclaration' &&
234
- parent.type !== 'VariableDeclarator' &&
235
- parent.type !== 'Attribute'
236
- ) {
237
- return unhoisted;
238
- }
239
- }
240
- }
241
-
242
- // If the binding is exported, bail out
243
- if (
244
- state.analysis?.exports?.find(
245
- (/** @type {{name: string}} */ node) => node.name === handler.name,
246
- )
247
- ) {
248
- return unhoisted;
249
- }
250
-
251
- if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
252
- const binding_type = binding.initial.type;
253
-
254
- if (
255
- binding_type === 'ArrowFunctionExpression' ||
256
- binding_type === 'FunctionDeclaration' ||
257
- binding_type === 'FunctionExpression'
258
- ) {
259
- target_function = binding.initial;
260
- }
261
- }
262
- }
263
-
264
- // If we can't find a function, or the function has multiple parameters, bail out
265
- if (target_function == null || target_function.params.length > 1) {
266
- return unhoisted;
267
- }
268
-
269
- const visited_references = new Set();
270
- const scope = target_function.metadata.scope;
271
- for (const [reference, ref_nodes] of scope.references) {
272
- // Bail out if the arguments keyword is used or $host is referenced
273
- if (reference === 'arguments') return unhoisted;
274
-
275
- const binding = scope.get(reference);
276
- const local_binding = state.scope.get(reference);
277
-
278
- if (local_binding === null || binding == null) {
279
- return unhoisted;
280
- }
281
-
282
- // If we are referencing a binding that is shadowed in another scope then bail out.
283
- if (local_binding.node !== binding.node) {
284
- return unhoisted;
285
- }
286
- const is_tracked = ref_nodes.some(({ node }) => node.tracked);
287
-
288
- if (
289
- binding !== null &&
290
- // Bail out if the the binding is a rest param
291
- (binding.declaration_kind === 'rest_param' || // or any normal not reactive bindings that are mutated.
292
- // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
293
- (binding.kind === 'normal' && !is_tracked && binding.updated))
294
- ) {
295
- return unhoisted;
296
- }
297
- visited_references.add(reference);
298
- }
299
-
300
- return { hoisted: true, function: target_function };
301
- }
302
-
303
- /**
304
- * @param {Node} node
305
- * @param {TransformContext} context
306
- * @returns {Identifier[]}
307
- */
308
- function get_hoisted_params(node, context) {
309
- const scope = context.state.scope;
310
-
311
- /** @type {Identifier[]} */
312
- const params = [];
313
-
314
- /**
315
- * We only want to push if it's not already present to avoid name clashing
316
- * @param {Identifier} id
317
- */
318
- function push_unique(id) {
319
- if (!params.find((param) => param.name === id.name)) {
320
- params.push(id);
321
- }
322
- }
323
-
324
- for (const [reference] of scope.references) {
325
- let binding = scope.get(reference);
326
-
327
- if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
328
- if (binding.kind === 'prop') {
329
- push_unique(b.id('__props'));
330
- } else if (binding.kind === 'for_pattern') {
331
- push_unique(binding.metadata.pattern);
332
- } else if (binding.kind === 'prop_fallback') {
333
- push_unique(b.id(binding.node.name));
334
- } else if (
335
- // imports don't need to be hoisted
336
- binding.declaration_kind !== 'import'
337
- ) {
338
- // create a copy to remove start/end tags which would mess up source maps
339
- push_unique(b.id(binding.node.name));
340
- }
341
- }
342
- }
343
- return params;
344
- }
345
-
346
- /**
347
- * Builds the parameter list for a hoisted function
348
- * @param {FunctionDeclaration|FunctionExpression|ArrowFunctionExpression} node
349
- * @param {TransformContext} context
350
- * @returns {Pattern[]}
351
- */
352
- export function build_hoisted_params(node, context) {
353
- const hoisted_params = get_hoisted_params(node, context);
354
- node.metadata.hoisted_params = hoisted_params;
355
-
356
- /** @type {Pattern[]} */
357
- const params = [];
358
-
359
- if (node.params.length === 0) {
360
- if (hoisted_params.length > 0) {
361
- // For the event object
362
- params.push(b.id(context.state.scope.generate('_')));
363
- }
364
- } else {
365
- for (const param of node.params) {
366
- params.push(/** @type {Pattern} */ (context.visit(param)));
367
- }
182
+ return false;
368
183
  }
369
-
370
- params.push(...hoisted_params, b.id('__block'));
371
- return params;
184
+ return true;
372
185
  }
373
186
 
374
187
  /**
@@ -1,7 +1,13 @@
1
1
  describe('SVG namespace handling', () => {
2
2
  it('should render static SVG elements with correct namespace', () => {
3
3
  component App() {
4
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor">
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ >
5
11
  <path d="m14 12 4 4 4-4" />
6
12
  <circle cx="12" cy="12" r="4" />
7
13
  <rect x="4" y="4" width="16" height="16" />
@@ -32,9 +38,20 @@ describe('SVG namespace handling', () => {
32
38
 
33
39
  it('should render dynamic SVG paths with for loop (original issue)', () => {
34
40
  component App() {
35
- const d = ["m14 12 4 4 4-4", "M18 16V7", "m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16", "M3.304 13h6.392"];
41
+ const d = [
42
+ 'm14 12 4 4 4-4',
43
+ 'M18 16V7',
44
+ 'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
45
+ 'M3.304 13h6.392',
46
+ ];
36
47
 
37
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor">
48
+ <svg
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ width="24"
51
+ height="24"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ >
38
55
  for (const pathData of d) {
39
56
  <path d={pathData} />
40
57
  }
@@ -52,7 +69,12 @@ describe('SVG namespace handling', () => {
52
69
 
53
70
  // Critical test: dynamic paths should have correct SVG namespace
54
71
  expect(paths.length).toBe(4);
55
- const expectedPaths = ["m14 12 4 4 4-4", "M18 16V7", "m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16", "M3.304 13h6.392"];
72
+ const expectedPaths = [
73
+ 'm14 12 4 4 4-4',
74
+ 'M18 16V7',
75
+ 'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
76
+ 'M3.304 13h6.392',
77
+ ];
56
78
  paths.forEach((path, i) => {
57
79
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
58
80
  expect(path.getAttribute('d')).toBe(expectedPaths[i]);
@@ -60,7 +82,7 @@ describe('SVG namespace handling', () => {
60
82
 
61
83
  // Verify paths are actually SVG elements (should have getBBox method)
62
84
  // Note: getBBox might not work in test environment, so just check namespace
63
- paths.forEach(path => {
85
+ paths.forEach((path) => {
64
86
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
65
87
  expect(path.tagName.toLowerCase()).toBe('path');
66
88
  });
@@ -68,9 +90,15 @@ describe('SVG namespace handling', () => {
68
90
 
69
91
  it('should handle mixed static and dynamic SVG elements', () => {
70
92
  component App() {
71
- const dynamicPaths = ["M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z"];
72
-
73
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor">
93
+ const dynamicPaths = ['M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z'];
94
+
95
+ <svg
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ width="24"
98
+ height="24"
99
+ fill="none"
100
+ stroke="currentColor"
101
+ >
74
102
  <circle cx="12" cy="12" r="10" />
75
103
  for (const pathData of dynamicPaths) {
76
104
  <path d={pathData} />
@@ -93,14 +121,16 @@ describe('SVG namespace handling', () => {
93
121
  expect(rect.namespaceURI).toBe('http://www.w3.org/2000/svg');
94
122
 
95
123
  // Verify content
96
- expect(path.getAttribute('d')).toBe('M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z');
124
+ expect(path.getAttribute('d')).toBe(
125
+ 'M12 2L2 7v10c0 5.55 3.84 10 9 11 5.16-1 9-5.45 9-11V7l-10-5z',
126
+ );
97
127
  });
98
128
 
99
129
  it('should handle nested SVG groups with for loops', () => {
100
130
  component App() {
101
131
  const items = [
102
- { x: "10", y: "10", width: "20", height: "20" },
103
- { x: "40", y: "40", width: "20", height: "20" }
132
+ { x: '10', y: '10', width: '20', height: '20' },
133
+ { x: '40', y: '40', width: '20', height: '20' },
104
134
  ];
105
135
 
106
136
  <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
@@ -123,7 +153,7 @@ describe('SVG namespace handling', () => {
123
153
  expect(g.namespaceURI).toBe('http://www.w3.org/2000/svg');
124
154
  expect(rects.length).toBe(2);
125
155
 
126
- rects.forEach(rect => {
156
+ rects.forEach((rect) => {
127
157
  expect(rect.namespaceURI).toBe('http://www.w3.org/2000/svg');
128
158
  });
129
159
 
@@ -200,22 +230,39 @@ describe('SVG namespace handling', () => {
200
230
 
201
231
  it('should compare static vs dynamic SVG rendering (original problem case)', () => {
202
232
  component App() {
203
- const d = ["m14 12 4 4 4-4", "M18 16V7", "m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16", "M3.304 13h6.392"];
233
+ const d = [
234
+ 'm14 12 4 4 4-4',
235
+ 'M18 16V7',
236
+ 'm2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16',
237
+ 'M3.304 13h6.392',
238
+ ];
204
239
 
205
240
  <div class="container">
206
- {/* Dynamic SVG - the original problem case */}
207
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" class="dynamic-svg">
241
+ <svg
242
+ xmlns="http://www.w3.org/2000/svg"
243
+ width="24"
244
+ height="24"
245
+ fill="none"
246
+ stroke="currentColor"
247
+ class="dynamic-svg"
248
+ >
208
249
  for (const path of d) {
209
250
  <path d={path} />
210
251
  }
211
252
  </svg>
212
253
 
213
- {/* Static SVG - always worked */}
214
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" stroke="currentColor" class="static-svg">
215
- <path d="m14 12 4 4 4-4"></path>
216
- <path d="M18 16V7"></path>
217
- <path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16"></path>
218
- <path d="M3.304 13h6.392"></path>
254
+ <svg
255
+ xmlns="http://www.w3.org/2000/svg"
256
+ width="24"
257
+ height="24"
258
+ fill="none"
259
+ stroke="currentColor"
260
+ class="static-svg"
261
+ >
262
+ <path d="m14 12 4 4 4-4" />
263
+ <path d="M18 16V7" />
264
+ <path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" />
265
+ <path d="M3.304 13h6.392" />
219
266
  </svg>
220
267
  </div>
221
268
  }
@@ -236,10 +283,10 @@ describe('SVG namespace handling', () => {
236
283
  expect(staticPaths.length).toBe(4);
237
284
 
238
285
  // All paths should have SVG namespace
239
- dynamicPaths.forEach(path => {
286
+ dynamicPaths.forEach((path) => {
240
287
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
241
288
  });
242
- staticPaths.forEach(path => {
289
+ staticPaths.forEach((path) => {
243
290
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
244
291
  });
245
292
 
@@ -249,11 +296,11 @@ describe('SVG namespace handling', () => {
249
296
  });
250
297
 
251
298
  // Critical test: all paths should be proper SVG elements
252
- dynamicPaths.forEach(path => {
299
+ dynamicPaths.forEach((path) => {
253
300
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
254
301
  expect(path.tagName.toLowerCase()).toBe('path');
255
302
  });
256
- staticPaths.forEach(path => {
303
+ staticPaths.forEach((path) => {
257
304
  expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
258
305
  expect(path.tagName.toLowerCase()).toBe('path');
259
306
  });