ripple 0.2.47 → 0.2.48

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 a TypeScript UI framework for the web",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.47",
6
+ "version": "0.2.48",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index.js",
9
9
  "main": "src/runtime/index.js",
@@ -25,6 +25,7 @@ function RipplePlugin(config) {
25
25
 
26
26
  class RippleParser extends Parser {
27
27
  #path = [];
28
+ skip_decorator = false;
28
29
 
29
30
  // Helper method to get the element name from a JSX identifier or member expression
30
31
  getElementName(node) {
@@ -274,6 +275,7 @@ function RipplePlugin(config) {
274
275
  }
275
276
  }
276
277
 
278
+
277
279
  parseTryStatement(node) {
278
280
  this.next();
279
281
  node.block = this.parseBlock();
@@ -702,6 +704,54 @@ function RipplePlugin(config) {
702
704
  return node;
703
705
  }
704
706
 
707
+ if (this.type.label === '@') {
708
+ // Try to parse as an expression statement first using tryParse
709
+ // This allows us to handle Ripple @ syntax like @count++ without
710
+ // interfering with legitimate decorator syntax
711
+ this.skip_decorator = true;
712
+ const expressionResult = this.tryParse(() => {
713
+ const node = this.startNode();
714
+ this.next();
715
+ // Force expression context to ensure @ is tokenized correctly
716
+ const oldExprAllowed = this.exprAllowed;
717
+ this.exprAllowed = true;
718
+ node.expression = this.parseExpression();
719
+
720
+ if (node.expression.type === 'UpdateExpression') {
721
+ let object = node.expression.argument;
722
+ while (object.type === 'MemberExpression') {
723
+ object = object.object;
724
+ }
725
+ if (object.type === 'Identifier') {
726
+ object.tracked = true;
727
+ }
728
+ } else if (node.expression.type === 'AssignmentExpression') {
729
+ let object = node.expression.left;
730
+ while (object.type === 'MemberExpression') {
731
+ object = object.object;
732
+ }
733
+ if (object.type === 'Identifier') {
734
+ object.tracked = true;
735
+ }
736
+ } else if (node.expression.type === 'Identifier') {
737
+ node.expression.tracked = true;
738
+ } else {
739
+ // TODO?
740
+ }
741
+
742
+ this.exprAllowed = oldExprAllowed;
743
+ return this.finishNode(node, 'ExpressionStatement');
744
+ });
745
+ this.skip_decorator = false;
746
+
747
+ // If parsing as expression statement succeeded, use that result
748
+ if (expressionResult.node) {
749
+ return expressionResult.node;
750
+ }
751
+
752
+ // Otherwise, fall back to default decorator parsing
753
+ }
754
+
705
755
  return super.parseStatement(context, topLevel, exports);
706
756
  }
707
757
 
@@ -269,7 +269,7 @@ const visitors = {
269
269
  const parent_node = path?.at(-1);
270
270
 
271
271
  // We're reading a computed property, which might mean it's a reactive property
272
- if (parent_node?.type === 'MemberExpression' && parent_node.computed) {
272
+ if (!ref.node.tracked && parent_node?.type === 'MemberExpression' && parent_node.computed) {
273
273
  binding.transform = {
274
274
  assign: (node, value, computed) => {
275
275
  if (!computed) {
@@ -120,30 +120,36 @@ const visitors = {
120
120
  Identifier(node, context) {
121
121
  const parent = /** @type {Node} */ (context.path.at(-1));
122
122
 
123
- if (is_reference(node, parent) && !context.state.to_ts) {
124
- const binding = context.state.scope.get(node.name);
125
- if (
126
- (context.state.metadata?.tracking === false ||
127
- (parent.type !== 'AssignmentExpression' && parent.type !== 'UpdateExpression')) &&
128
- (is_tracked_name(node.name) ||
129
- node.tracked ||
130
- binding?.kind === 'prop' ||
131
- binding?.kind === 'prop_fallback') &&
132
- binding?.node !== node
133
- ) {
134
- if (context.state.metadata?.tracking === false) {
135
- context.state.metadata.tracking = true;
123
+ if (is_reference(node, parent)) {
124
+ if (context.state.to_ts) {
125
+ if (node.tracked) {
126
+ return b.member(node, b.literal('#v'), true)
127
+ }
128
+ } else {
129
+ const binding = context.state.scope.get(node.name);
130
+ if (
131
+ (context.state.metadata?.tracking === false ||
132
+ (parent.type !== 'AssignmentExpression' && parent.type !== 'UpdateExpression')) &&
133
+ (is_tracked_name(node.name) ||
134
+ node.tracked ||
135
+ binding?.kind === 'prop' ||
136
+ binding?.kind === 'prop_fallback') &&
137
+ binding?.node !== node
138
+ ) {
139
+ if (context.state.metadata?.tracking === false) {
140
+ context.state.metadata.tracking = true;
141
+ }
142
+ if (node.tracked) {
143
+ return b.call('$.get', build_getter(node, context));
144
+ }
136
145
  }
137
- if (node.tracked) {
138
- return b.call('$.get', build_getter(node, context));
146
+
147
+ if (node.name === 'structuredClone' && binding === null) {
148
+ return b.id('$.structured_clone');
139
149
  }
140
- }
141
150
 
142
- if (node.name === 'structuredClone' && binding === null) {
143
- return b.id('$.structured_clone');
151
+ return build_getter(node, context);
144
152
  }
145
-
146
- return build_getter(node, context);
147
153
  }
148
154
  },
149
155
 
@@ -435,6 +435,9 @@ export function is_value_static(node) {
435
435
  }
436
436
 
437
437
  export function is_tracked_computed_property(object, property, context) {
438
+ if (object.tracked) {
439
+ return false;
440
+ }
438
441
  const binding = context.state.scope.get(object.name);
439
442
 
440
443
  if (binding) {
@@ -693,13 +696,13 @@ const common_dom_names = new Set([
693
696
 
694
697
  export function is_element_dom_element(node, context) {
695
698
  if (node.id.type === 'Identifier' && node.id.name[0].toLowerCase() === node.id.name[0]) {
696
- if (common_dom_names.has(node.id.name)) {
697
- return true;
698
- }
699
- const binding = context.state.scope.get(node.id.name);
699
+ if (common_dom_names.has(node.id.name)) {
700
+ return true;
701
+ }
702
+ const binding = context.state.scope.get(node.id.name);
700
703
  if (binding == null) {
701
- return true;
702
- }
704
+ return true;
705
+ }
703
706
  }
704
707
  return false;
705
708
  }
@@ -163,7 +163,7 @@ function push_block(block, parent_block) {
163
163
  export function block(flags, fn, state = null) {
164
164
  /** @type {Block} */
165
165
  var block = {
166
- c: active_component,
166
+ co: active_component,
167
167
  d: null,
168
168
  first: null,
169
169
  f: flags,
@@ -170,7 +170,7 @@ function run_derived(computed) {
170
170
  active_reaction = computed;
171
171
  tracking = true;
172
172
  active_dependency = null;
173
- active_component = active_block.c;
173
+ active_component = computed.co;
174
174
 
175
175
  destroy_computed_children(computed);
176
176
 
@@ -221,7 +221,7 @@ export function run_block(block) {
221
221
  try {
222
222
  active_block = block;
223
223
  active_reaction = block;
224
- active_component = block.c;
224
+ active_component = block.co;
225
225
 
226
226
  destroy_non_branch_children(block);
227
227
  run_teardown(block);
@@ -279,6 +279,7 @@ export function derived(fn, block) {
279
279
  b: block,
280
280
  blocks: null,
281
281
  c: 0,
282
+ co: active_component,
282
283
  d: null,
283
284
  f: TRACKED | DERIVED,
284
285
  fn,
@@ -28,6 +28,7 @@ export type Derived = {
28
28
  b: Block;
29
29
  blocks: null | Block[];
30
30
  c: number;
31
+ co: null | Component;
31
32
  d: null;
32
33
  f: number;
33
34
  fn: Function;
@@ -35,7 +36,7 @@ export type Derived = {
35
36
  };
36
37
 
37
38
  export type Block = {
38
- c: null | Component;
39
+ co: null | Component;
39
40
  d: null | Dependency;
40
41
  first: null | Block;
41
42
  f: number;
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, RippleArray } from 'ripple';
2
+ import { mount, RippleArray, track } from 'ripple';
3
3
  import { parse } from 'ripple/compiler'
4
4
 
5
5
  describe('compiler success tests', () => {
@@ -109,15 +109,15 @@ describe('compiler success tests', () => {
109
109
 
110
110
  component App() {
111
111
  let items = [];
112
- let $items = [];
112
+ let tracked_items = track([]);
113
113
  let items2 = new Array();
114
114
  let items3 = new RippleArray();
115
115
  let i = 0;
116
116
 
117
117
  logs.push(items[0]);
118
118
  logs.push(items[i]);
119
- logs.push($items[0]);
120
- logs.push($items[i]);
119
+ logs.push(@tracked_items[0]);
120
+ logs.push(@tracked_items[i]);
121
121
  logs.push(items2[0]);
122
122
  logs.push(items2[i]);
123
123
  logs.push(items3[0]);
@@ -125,8 +125,8 @@ describe('compiler success tests', () => {
125
125
 
126
126
  items[0] = 123;
127
127
  items[i] = 123;
128
- $items[0] = 123;
129
- $items[i] = 123;
128
+ @tracked_items[0] = 123;
129
+ @tracked_items[i] = 123;
130
130
  items2[0] = 123;
131
131
  items2[i] = 123;
132
132
  items3[0] = 123;
@@ -134,8 +134,8 @@ describe('compiler success tests', () => {
134
134
 
135
135
  logs.push(items[0]);
136
136
  logs.push(items[i]);
137
- logs.push($items[0]);
138
- logs.push($items[i]);
137
+ logs.push(@tracked_items[0]);
138
+ logs.push(@tracked_items[i]);
139
139
  logs.push(items2[0]);
140
140
  logs.push(items2[i]);
141
141
  logs.push(items3[0]);
@@ -143,8 +143,8 @@ describe('compiler success tests', () => {
143
143
 
144
144
  items[0]++;
145
145
  items[i]++;
146
- $items[0]++;
147
- $items[i]++;
146
+ @tracked_items[0]++;
147
+ @tracked_items[i]++;
148
148
  items2[0]++;
149
149
  items2[i]++;
150
150
  items3[0]++;
@@ -152,8 +152,8 @@ describe('compiler success tests', () => {
152
152
 
153
153
  logs.push(items[0]);
154
154
  logs.push(items[i]);
155
- logs.push($items[0]);
156
- logs.push($items[i]);
155
+ logs.push(@tracked_items[0]);
156
+ logs.push(@tracked_items[i]);
157
157
  logs.push(items2[0]);
158
158
  logs.push(items2[i]);
159
159
  logs.push(items3[0]);
@@ -161,8 +161,8 @@ describe('compiler success tests', () => {
161
161
 
162
162
  logs.push(--items[0]);
163
163
  logs.push(--items[i]);
164
- logs.push(--$items[0]);
165
- logs.push(--$items[i]);
164
+ logs.push(--@tracked_items[0]);
165
+ logs.push(--@tracked_items[i]);
166
166
  logs.push(--items2[0]);
167
167
  logs.push(--items2[i]);
168
168
  logs.push(--items3[0]);
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, createContext } from 'ripple';
2
+ import { mount, createContext, flushSync, track } from 'ripple';
3
3
 
4
4
  describe('context', () => {
5
5
  let container;
@@ -19,25 +19,48 @@ describe('context', () => {
19
19
  document.body.removeChild(container);
20
20
  });
21
21
 
22
- it('creates a reactive ref with initial value', () => {
23
- const MyContext = createContext(null);
22
+ it('creates a reactive ref with initial value', () => {
23
+ const MyContext = createContext(null);
24
24
 
25
- component Child() {
26
- const value = MyContext.get();
25
+ component Child() {
26
+ const value = MyContext.get();
27
27
 
28
- <div>{value}</div>
29
- }
28
+ <div>{value}</div>
29
+ }
30
30
 
31
- component TestContext() {
32
- const value = MyContext.get();
31
+ component TestContext() {
32
+ const value = MyContext.get();
33
33
 
34
- MyContext.set("Hello from context!");
34
+ MyContext.set("Hello from context!");
35
35
 
36
- <Child />
37
- }
36
+ <Child />
37
+ }
38
38
 
39
39
  render(TestContext);
40
40
 
41
41
  expect(container.querySelector('div').textContent).toBe('Hello from context!');
42
42
  });
43
+
44
+ it('handles context captured inside a computed tracked', () => {
45
+
46
+ const MyContext = createContext(null)
47
+
48
+ const doubleContext = () => {
49
+ const value = MyContext.get()
50
+ return value * 2
51
+ }
52
+
53
+ component App() {
54
+ MyContext.set(4)
55
+
56
+ <h3>{MyContext.get()}</h3>
57
+
58
+ <h4>{'2x:'} {doubleContext()}</h4>
59
+
60
+ MyContext.set(8)
61
+ }
62
+
63
+ render(App);
64
+ flushSync();
65
+ });
43
66
  });
package/types/index.d.ts CHANGED
@@ -16,13 +16,13 @@ export declare class RippleArray<T> extends Array<T> {
16
16
  static from<T, U>(
17
17
  arrayLike: ArrayLike<T>,
18
18
  mapFn: (v: T, k: number) => U,
19
- thisArg?: any
19
+ thisArg?: any,
20
20
  ): RippleArray<U>;
21
21
  static from<T>(iterable: Iterable<T>): RippleArray<T>;
22
22
  static from<T, U>(
23
23
  iterable: Iterable<T>,
24
24
  mapFn: (v: T, k: number) => U,
25
- thisArg?: any
25
+ thisArg?: any,
26
26
  ): RippleArray<U>;
27
27
 
28
28
  static of<T>(...items: T[]): RippleArray<T>;
@@ -40,48 +40,48 @@ export type Context<T> = {
40
40
  export declare function createContext<T>(initialValue: T): Context<T>;
41
41
 
42
42
  export class RippleSet<T> extends Set<T> {
43
- readonly $size: number;
44
- isDisjointFrom(other: RippleSet<T> | Set<T>): boolean;
45
- isSubsetOf(other: RippleSet<T> | Set<T>): boolean;
46
- isSupersetOf(other: RippleSet<T> | Set<T>): boolean;
47
- difference(other: RippleSet<T> | Set<T>): RippleSet<T>;
48
- intersection(other: RippleSet<T> | Set<T>): RippleSet<T>;
49
- symmetricDifference(other: RippleSet<T> | Set<T>): RippleSet<T>;
50
- union(other: RippleSet<T> | Set<T>): RippleSet<T>;
51
- toJSON(): T[];
43
+ readonly $size: number;
44
+ isDisjointFrom(other: RippleSet<T> | Set<T>): boolean;
45
+ isSubsetOf(other: RippleSet<T> | Set<T>): boolean;
46
+ isSupersetOf(other: RippleSet<T> | Set<T>): boolean;
47
+ difference(other: RippleSet<T> | Set<T>): RippleSet<T>;
48
+ intersection(other: RippleSet<T> | Set<T>): RippleSet<T>;
49
+ symmetricDifference(other: RippleSet<T> | Set<T>): RippleSet<T>;
50
+ union(other: RippleSet<T> | Set<T>): RippleSet<T>;
51
+ toJSON(): T[];
52
52
  }
53
53
 
54
54
  export class RippleMap<K, V> extends Map<K, V> {
55
- get $size(): number;
56
- toJSON(): [K, V][];
55
+ get $size(): number;
56
+ toJSON(): [K, V][];
57
57
  }
58
58
 
59
59
  // Compiler-injected runtime symbols (for Ripple component development)
60
60
  declare global {
61
- /**
62
- * Runtime block context injected by the Ripple compiler.
63
- * This is automatically available in component scopes and passed to runtime functions.
64
- */
65
- var __block: any;
66
-
67
- /**
68
- * Ripple runtime namespace - injected by the compiler
69
- * These functions are available in compiled Ripple components for TypeScript analysis
70
- */
71
- var $: {
72
- tracked<T>(value: T, block?: any): T;
73
- tracked_object<T extends Record<string, any>>(obj: T, props: string[], block?: any): T;
74
- computed<T>(fn: () => T, block?: any): T;
75
- scope(): any;
76
- get_tracked(node: any): any;
77
- get_derived(node: any): any;
78
- set(node: any, value: any, block?: any): any;
79
- // Add other runtime functions as needed for TypeScript analysis
80
- };
61
+ /**
62
+ * Runtime block context injected by the Ripple compiler.
63
+ * This is automatically available in component scopes and passed to runtime functions.
64
+ */
65
+ var __block: any;
66
+
67
+ /**
68
+ * Ripple runtime namespace - injected by the compiler
69
+ * These functions are available in compiled Ripple components for TypeScript analysis
70
+ */
71
+ var $: {
72
+ tracked<T>(value: T, block?: any): T;
73
+ tracked_object<T extends Record<string, any>>(obj: T, props: string[], block?: any): T;
74
+ computed<T>(fn: () => T, block?: any): T;
75
+ scope(): any;
76
+ get_tracked(node: any): any;
77
+ get_derived(node: any): any;
78
+ set(node: any, value: any, block?: any): any;
79
+ // Add other runtime functions as needed for TypeScript analysis
80
+ };
81
81
  }
82
82
 
83
83
  export declare function createRefKey(): symbol;
84
84
 
85
- type Tracked<V> = {};
85
+ type Tracked<V> = { '#v': V };
86
86
 
87
- export declare function track<V>(value: V): Tracked<V>;
87
+ export declare function track<V>(value: V | (() => V)): Tracked<V>;