ripple 0.2.212 → 0.2.213

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 (36) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +2 -2
  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 +0 -1
  6. package/src/compiler/types/index.d.ts +3 -1
  7. package/tests/client/compiler/compiler.basic.test.ripple +24 -0
  8. package/tests/hydration/build-components.js +2 -2
  9. package/tests/hydration/compiled/client/basic.js +2 -1
  10. package/tests/hydration/compiled/client/composite.js +1 -0
  11. package/tests/hydration/compiled/client/events.js +1 -0
  12. package/tests/hydration/compiled/client/for.js +1 -0
  13. package/tests/hydration/compiled/client/head.js +1 -0
  14. package/tests/hydration/compiled/client/html.js +1 -0
  15. package/tests/hydration/compiled/client/if-children.js +1 -0
  16. package/tests/hydration/compiled/client/if.js +1 -0
  17. package/tests/hydration/compiled/client/portal.js +1 -0
  18. package/tests/hydration/compiled/client/reactivity.js +1 -0
  19. package/tests/hydration/compiled/client/return.js +1 -0
  20. package/tests/hydration/compiled/client/switch.js +1 -0
  21. package/tests/hydration/compiled/server/basic.js +2 -1
  22. package/tests/hydration/compiled/server/composite.js +1 -0
  23. package/tests/hydration/compiled/server/events.js +1 -0
  24. package/tests/hydration/compiled/server/for.js +1 -0
  25. package/tests/hydration/compiled/server/head.js +1 -0
  26. package/tests/hydration/compiled/server/html.js +1 -0
  27. package/tests/hydration/compiled/server/if-children.js +1 -0
  28. package/tests/hydration/compiled/server/if.js +1 -0
  29. package/tests/hydration/compiled/server/portal.js +1 -0
  30. package/tests/hydration/compiled/server/reactivity.js +1 -0
  31. package/tests/hydration/compiled/server/return.js +1 -0
  32. package/tests/hydration/compiled/server/switch.js +1 -0
  33. package/tests/hydration/components/basic.ripple +2 -1
  34. package/tests/hydration/components/composite.ripple +3 -1
  35. package/tests/hydration/tsconfig.json +12 -0
  36. package/tests/hydration.d.ts +14 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # ripple
2
2
 
3
+ ## 0.2.213
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies []:
8
+ - ripple@0.2.213
9
+
3
10
  ## 0.2.212
4
11
 
5
12
  ### 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.213",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -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.213"
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] };
@@ -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 {
@@ -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
  });
@@ -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);
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/client';
2
3
 
3
4
  var root = _$_.template(`<div><!></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(`<div class="content"><!></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(`<div class="shown">Visible</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(`<div class="portal-content">Portal content</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 class="count"> </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(`<div class="after">after</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(`<div class="status-success">Success</div>`, 1);
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  export function StaticText(__output) {
@@ -345,7 +346,7 @@ function Actions(__output, { playgroundVisible = false }) {
345
346
  __output.push('</a>');
346
347
  __output.push('<!--[-->');
347
348
 
348
- if (_$_.get(playgroundVisible)) {
349
+ if (playgroundVisible) {
349
350
  __output.push('<a');
350
351
  __output.push(' href="/playground"');
351
352
  __output.push(' class="playground-link"');
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  export async function Layout(__output, { children }) {
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { Portal } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  import * as _$_ from 'ripple/internal/server';
2
3
 
3
4
  import { track } from 'ripple/server';
@@ -1,4 +1,5 @@
1
1
  // Basic static components for hydration testing
2
+ import type { Component } from 'ripple';
2
3
 
3
4
  export component StaticText() {
4
5
  <div>{'Hello World'}</div>
@@ -96,7 +97,7 @@ component Actions({ playgroundVisible = false }: { playgroundVisible: boolean })
96
97
  </div>
97
98
  }
98
99
 
99
- component Layout({ children }) {
100
+ component Layout({ children }: { children: Component }) {
100
101
  <main>
101
102
  <div class="container">
102
103
  <children />
@@ -1,4 +1,6 @@
1
- export component Layout({ children }) {
1
+ import type { Component } from 'ripple';
2
+
3
+ export component Layout({ children }: { children?: Component }) {
2
4
  <div class="layout">
3
5
  <children />
4
6
  </div>
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["../../src/**/*", "../**/*"],
4
+ "exclude": [
5
+ "../client",
6
+ "../client.d.ts",
7
+ "../setup-client.js",
8
+ "../server",
9
+ "../server.d.ts",
10
+ "../setup-server.js"
11
+ ]
12
+ }
@@ -0,0 +1,14 @@
1
+ export {};
2
+
3
+ interface CustomMatchers<R = unknown> {
4
+ /**
5
+ * Compares HTML strings after stripping hydration markers.
6
+ * Hydration markers are: <!--[--> <!--[!--> <!--]-->
7
+ */
8
+ toBeHtml(expected: string): R;
9
+ }
10
+
11
+ declare module 'vitest' {
12
+ interface Assertion<T = any> extends CustomMatchers<T> {}
13
+ interface AsymmetricMatchersContaining extends CustomMatchers {}
14
+ }