ripple 0.2.212 → 0.2.214

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +3 -3
  3. package/src/compiler/phases/1-parse/index.js +50 -2
  4. package/src/compiler/phases/2-analyze/index.js +13 -29
  5. package/src/compiler/phases/3-transform/client/index.js +5 -2
  6. package/src/compiler/types/index.d.ts +3 -1
  7. package/src/runtime/internal/client/hmr.js +1 -0
  8. package/src/runtime/internal/client/html.js +1 -1
  9. package/src/runtime/internal/client/template.js +5 -5
  10. package/tests/client/compiler/compiler.basic.test.ripple +24 -0
  11. package/tests/client/svg.test.ripple +44 -0
  12. package/tests/hydration/build-components.js +2 -2
  13. package/tests/hydration/compiled/client/basic.js +2 -1
  14. package/tests/hydration/compiled/client/composite.js +1 -0
  15. package/tests/hydration/compiled/client/events.js +1 -0
  16. package/tests/hydration/compiled/client/for.js +1 -0
  17. package/tests/hydration/compiled/client/head.js +1 -0
  18. package/tests/hydration/compiled/client/html.js +527 -0
  19. package/tests/hydration/compiled/client/if-children.js +1 -0
  20. package/tests/hydration/compiled/client/if.js +1 -0
  21. package/tests/hydration/compiled/client/portal.js +1 -0
  22. package/tests/hydration/compiled/client/reactivity.js +1 -0
  23. package/tests/hydration/compiled/client/return.js +1 -0
  24. package/tests/hydration/compiled/client/switch.js +28 -27
  25. package/tests/hydration/compiled/server/basic.js +2 -1
  26. package/tests/hydration/compiled/server/composite.js +1 -0
  27. package/tests/hydration/compiled/server/events.js +1 -0
  28. package/tests/hydration/compiled/server/for.js +1 -0
  29. package/tests/hydration/compiled/server/head.js +1 -0
  30. package/tests/hydration/compiled/server/html.js +530 -0
  31. package/tests/hydration/compiled/server/if-children.js +1 -0
  32. package/tests/hydration/compiled/server/if.js +1 -0
  33. package/tests/hydration/compiled/server/portal.js +1 -0
  34. package/tests/hydration/compiled/server/reactivity.js +1 -0
  35. package/tests/hydration/compiled/server/return.js +1 -0
  36. package/tests/hydration/compiled/server/switch.js +1 -0
  37. package/tests/hydration/components/basic.ripple +2 -1
  38. package/tests/hydration/components/composite.ripple +3 -1
  39. package/tests/hydration/components/html.ripple +129 -0
  40. package/tests/hydration/html.test.js +110 -0
  41. package/tests/hydration/tsconfig.json +12 -0
  42. package/tests/hydration.d.ts +14 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # ripple
2
2
 
3
+ ## 0.2.214
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.2.214
9
+
10
+ ## 0.2.213
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies []:
15
+ - ripple@0.2.213
16
+
3
17
  ## 0.2.212
4
18
 
5
19
  ### Patch Changes
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.212",
6
+ "version": "0.2.214",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -15,7 +15,7 @@
15
15
  "bugs": {
16
16
  "url": "https://github.com/Ripple-TS/ripple/issues"
17
17
  },
18
- "homepage": "https://ripplejs.com",
18
+ "homepage": "https://ripple-ts.com",
19
19
  "keywords": [
20
20
  "ripple",
21
21
  "UI",
@@ -96,6 +96,6 @@
96
96
  "vscode-languageserver-types": "^3.17.5"
97
97
  },
98
98
  "peerDependencies": {
99
- "ripple": "0.2.212"
99
+ "ripple": "0.2.214"
100
100
  }
101
101
  }
@@ -2667,7 +2667,7 @@ function get_comment_handlers(source, comments, index = 0) {
2667
2667
  },
2668
2668
 
2669
2669
  /**
2670
- * @param {AST.Node} ast
2670
+ * @param {AST.Node | AST.CSS.StyleSheet} ast
2671
2671
  */
2672
2672
  add_comments: (ast) => {
2673
2673
  if (comments.length === 0) return;
@@ -2685,7 +2685,7 @@ function get_comment_handlers(source, comments, index = 0) {
2685
2685
 
2686
2686
  walk(ast, null, {
2687
2687
  _(node, { next, path }) {
2688
- const metadata = node?.metadata;
2688
+ const metadata = /** @type {AST.Node} */ (node)?.metadata;
2689
2689
 
2690
2690
  /**
2691
2691
  * Check if a comment is inside an attribute expression
@@ -2758,6 +2758,36 @@ function get_comment_handlers(source, comments, index = 0) {
2758
2758
  return element;
2759
2759
  }
2760
2760
 
2761
+ // Skip CSS nodes entirely - they use CSS-local positions (relative to
2762
+ // the <style> tag content) which would incorrectly match against
2763
+ // absolute source positions of JS/HTML comments. Also consume any
2764
+ // CSS comments (which have absolute positions) that fall within the
2765
+ // parent <style> element's content range so they don't leak to
2766
+ // subsequent JS nodes.
2767
+ if (node.type === 'StyleSheet') {
2768
+ const styleElement = /** @type {AST.Element & AST.NodeWithLocation | undefined} */ (
2769
+ path.findLast(
2770
+ (ancestor) =>
2771
+ ancestor &&
2772
+ ancestor.type === 'Element' &&
2773
+ ancestor.id &&
2774
+ /** @type {AST.Identifier} */ (ancestor.id).name === 'style',
2775
+ )
2776
+ );
2777
+ if (styleElement) {
2778
+ const cssStart =
2779
+ /** @type {AST.NodeWithLocation} */ (styleElement.openingElement)?.end ??
2780
+ styleElement.start;
2781
+ const cssEnd =
2782
+ /** @type {AST.NodeWithLocation} */ (styleElement.closingElement)?.start ??
2783
+ styleElement.end;
2784
+ while (comments[0] && comments[0].start >= cssStart && comments[0].end <= cssEnd) {
2785
+ comments.shift();
2786
+ }
2787
+ }
2788
+ return;
2789
+ }
2790
+
2761
2791
  if (metadata && metadata.commentContainerId !== undefined) {
2762
2792
  // For empty template elements, keep comments as `innerComments`.
2763
2793
  // The Prettier plugin uses `innerComments` to preserve them and
@@ -2771,6 +2801,24 @@ function get_comment_handlers(source, comments, index = 0) {
2771
2801
  comments[0].context.containerId === metadata.commentContainerId &&
2772
2802
  comments[0].context.beforeMeaningfulChild
2773
2803
  ) {
2804
+ // Check that the comment is actually in this element's own content
2805
+ // area, not positionally inside a child element. This handles the
2806
+ // case where jsx_parseOpeningElementAt() triggers jsx_readToken()
2807
+ // before the child element is pushed to the parser's #path, causing
2808
+ // comments inside the child to get the parent's containerId.
2809
+ const commentStart = comments[0].start;
2810
+ const isInsideChildElement = /** @type {AST.NodeWithChildren} */ (
2811
+ node
2812
+ ).children?.some(
2813
+ (child) =>
2814
+ child &&
2815
+ child.start !== undefined &&
2816
+ child.end !== undefined &&
2817
+ commentStart >= child.start &&
2818
+ commentStart < child.end,
2819
+ );
2820
+ if (isInsideChildElement) break;
2821
+
2774
2822
  const elementComment = /** @type {AST.CommentWithLocation} */ (comments.shift());
2775
2823
 
2776
2824
  (metadata.elementLeadingComments ||= []).push(elementComment);
@@ -285,35 +285,19 @@ const visitors = {
285
285
  }
286
286
  }
287
287
 
288
- if (
289
- binding?.kind === 'prop' ||
290
- binding?.kind === 'prop_fallback' ||
291
- binding?.kind === 'for_pattern'
292
- ) {
293
- mark_as_tracked(context.path);
294
- if (context.state.metadata?.tracking === false) {
295
- context.state.metadata.tracking = true;
296
- }
297
- }
298
-
299
- if (
300
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
301
- node.tracked &&
302
- binding?.node !== node
303
- ) {
304
- mark_as_tracked(context.path);
305
- if (context.state.metadata?.tracking === false) {
306
- context.state.metadata.tracking = true;
307
- }
308
- }
309
-
310
- if (
311
- is_reference(node, /** @type {AST.Node} */ (parent)) &&
312
- node.tracked &&
313
- binding?.node !== node
314
- ) {
315
- if (context.state.metadata?.tracking === false) {
316
- context.state.metadata.tracking = true;
288
+ if (node.tracked && binding) {
289
+ if (
290
+ binding.kind === 'prop' ||
291
+ binding.kind === 'prop_fallback' ||
292
+ binding.kind === 'for_pattern' ||
293
+ (is_reference(node, /** @type {AST.Node} */ (parent)) &&
294
+ node.tracked &&
295
+ binding.node !== node)
296
+ ) {
297
+ mark_as_tracked(context.path);
298
+ if (context.state.metadata?.tracking === false) {
299
+ context.state.metadata.tracking = true;
300
+ }
317
301
  }
318
302
  }
319
303
 
@@ -116,7 +116,6 @@ function visit_function(node, context) {
116
116
  if (
117
117
  metadata?.tracked === true &&
118
118
  !is_inside_component(context, true) &&
119
- is_component_level_function(context) &&
120
119
  body.type === 'BlockStatement'
121
120
  ) {
122
121
  body = { ...body, body: [b.var('__block', b.call('_$_.scope')), ...body.body] };
@@ -3130,7 +3129,11 @@ function transform_children(children, context) {
3130
3129
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
3131
3130
  ) ||
3132
3131
  normalized.filter(
3133
- (node) => node.type !== 'VariableDeclaration' && node.type !== 'EmptyStatement',
3132
+ (node) =>
3133
+ node.type !== 'VariableDeclaration' &&
3134
+ node.type !== 'EmptyStatement' &&
3135
+ node.type !== 'BreakStatement' &&
3136
+ node.type !== 'ContinueStatement',
3134
3137
  ).length > 1;
3135
3138
  /** @type {AST.Identifier | null} */
3136
3139
  let initial = null;
@@ -394,7 +394,9 @@ declare module 'estree' {
394
394
  value: AST.Property['value'] | Component;
395
395
  }
396
396
 
397
- export type RippleAttribute = Attribute | SpreadAttribute | RefAttribute;
397
+ export type RippleAttribute = AST.Attribute | AST.SpreadAttribute | AST.RefAttribute;
398
+
399
+ export type NodeWithChildren = AST.Element | AST.TsxCompat;
398
400
 
399
401
  export namespace CSS {
400
402
  export interface BaseNode {
@@ -0,0 +1 @@
1
+ // TODO
@@ -20,7 +20,7 @@ export function html(node, get_html, svg = false, mathml = false) {
20
20
 
21
21
  render(() => {
22
22
  var block = /** @type {Block} */ (active_block);
23
- var new_html = get_html() + '';
23
+ var new_html = (get_html() ?? '') + '';
24
24
 
25
25
  // If the HTML hasn't changed, skip the update (but still hydrate on first run)
26
26
  if (html === new_html) {
@@ -63,11 +63,11 @@ export function create_fragment_from_html(
63
63
  export function template(content, flags) {
64
64
  var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
65
65
  var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
66
- var use_svg_namespace = (flags & TEMPLATE_SVG_NAMESPACE) !== 0;
67
- var use_mathml_namespace = (flags & TEMPLATE_MATHML_NAMESPACE) !== 0;
66
+ var is_comment = content === '<!>';
67
+ var use_svg_namespace = !is_comment && (flags & TEMPLATE_SVG_NAMESPACE) !== 0;
68
+ var use_mathml_namespace = !is_comment && (flags & TEMPLATE_MATHML_NAMESPACE) !== 0;
68
69
  /** @type {Node | DocumentFragment | undefined} */
69
70
  var node;
70
- var is_comment = content === '<!>';
71
71
  var has_start = !is_comment && !content.startsWith('<!>');
72
72
 
73
73
  // For fragments, eagerly create the node so we can walk its children
@@ -147,8 +147,8 @@ export function template(content, flags) {
147
147
  return /** @type {Node} */ (hydrate_node);
148
148
  }
149
149
  // If using runtime namespace, check active_namespace
150
- var svg = !is_comment && (use_svg_namespace || active_namespace === 'svg');
151
- var mathml = !is_comment && (use_mathml_namespace || active_namespace === 'mathml');
150
+ var svg = use_svg_namespace || (!is_comment && active_namespace === 'svg');
151
+ var mathml = use_mathml_namespace || (!is_comment && active_namespace === 'mathml');
152
152
 
153
153
  if (node === undefined || use_svg_namespace !== svg || use_mathml_namespace !== mathml) {
154
154
  node = create_fragment_from_html(has_start ? content : '<!>' + content, svg, mathml);
@@ -357,4 +357,28 @@ export component App() {
357
357
  `;
358
358
  expect(() => compile(code, 'test.ripple')).not.toThrow();
359
359
  });
360
+
361
+ it('should inject __block for track() calls inside class constructors', () => {
362
+ const source = `
363
+ import { track } from 'ripple';
364
+
365
+ class Store {
366
+ constructor() {
367
+ this.count = track(0);
368
+ this.items = #[1, 2, 3];
369
+ }
370
+ }
371
+
372
+ export component App() {
373
+ const store = new Store();
374
+ <div>{store.count}</div>
375
+ }
376
+ `;
377
+ const result = compile(source, 'test.ripple', { mode: 'client' });
378
+ const code = result.js.code;
379
+
380
+ // The constructor's compiled output should contain __block = _$_.scope()
381
+ expect(code).toContain('__block');
382
+ expect(code).toContain('_$_.scope()');
383
+ });
360
384
  });
@@ -438,4 +438,48 @@ describe('SVG namespace handling', () => {
438
438
  expect(path.tagName.toLowerCase()).toBe('path');
439
439
  });
440
440
  });
441
+
442
+ it('should render dynamic SVG elements dispatched via a switch inside a for loop', () => {
443
+ component App() {
444
+ const iconNodes: [string, Record<string, string>][] = [
445
+ ['path', { d: 'm14 12 4 4 4-4' }],
446
+ ['circle', { cx: '12', cy: '12', r: '4' }],
447
+ ['path', { d: 'M18 16V7' }],
448
+ ];
449
+
450
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
451
+ for (const [tag, attrs] of iconNodes) {
452
+ switch (tag) {
453
+ case 'path':
454
+ <path {...attrs} />
455
+ break;
456
+ case 'circle':
457
+ <circle {...attrs} />
458
+ break;
459
+ }
460
+ }
461
+ </svg>
462
+ }
463
+
464
+ render(App);
465
+
466
+ const svg = container.querySelector('svg');
467
+ expect(svg).toBeTruthy();
468
+ expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg');
469
+
470
+ const paths = svg.querySelectorAll('path');
471
+ const circles = svg.querySelectorAll('circle');
472
+
473
+ expect(paths.length).toBe(2);
474
+ expect(circles.length).toBe(1);
475
+
476
+ paths.forEach((path) => {
477
+ expect(path.namespaceURI).toBe('http://www.w3.org/2000/svg');
478
+ });
479
+ expect(circles[0].namespaceURI).toBe('http://www.w3.org/2000/svg');
480
+
481
+ expect(paths[0].getAttribute('d')).toBe('m14 12 4 4 4-4');
482
+ expect(circles[0].getAttribute('cx')).toBe('12');
483
+ expect(paths[1].getAttribute('d')).toBe('M18 16V7');
484
+ });
441
485
  });
@@ -57,7 +57,7 @@ function buildComponents() {
57
57
  const clientResult = compile(source, file, {
58
58
  mode: 'client',
59
59
  });
60
- writeFileSync(join(clientOutDir, outputName), clientResult.js.code);
60
+ writeFileSync(join(clientOutDir, outputName), '// @ts-nocheck\n' + clientResult.js.code);
61
61
 
62
62
  // Compile for server
63
63
  const serverResult = compile(source, file, {
@@ -65,7 +65,7 @@ function buildComponents() {
65
65
  });
66
66
  // Transform imports to use server runtime
67
67
  const serverCode = transformServerImports(serverResult.js.code);
68
- writeFileSync(join(serverOutDir, outputName), serverCode);
68
+ writeFileSync(join(serverOutDir, outputName), '// @ts-nocheck\n' + serverCode);
69
69
 
70
70
  console.log(`Compiled ${file} -> client & server`);
71
71
  }
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root = _$_.template(`<div>Hello World</div>`, 0);
@@ -255,7 +256,7 @@ function Actions(__anchor, __props, __block) {
255
256
  };
256
257
 
257
258
  _$_.if(node_5, (__render) => {
258
- if (_$_.get(_$_.fallback(__props.playgroundVisible, false))) __render(consequent);
259
+ if (_$_.fallback(__props.playgroundVisible, false)) __render(consequent);
259
260
  });
260
261
  }
261
262
 
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root = _$_.template(`<div class="layout"><!></div>`, 0);
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root = _$_.template(`<div><button class="increment">Increment</button><span class="count"> </span></div>`, 0);
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root_1 = _$_.template(`<li> </li>`, 0);
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root = _$_.template(`<div>Content</div>`, 0);