ripple 0.2.104 → 0.2.105

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.104",
6
+ "version": "0.2.105",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -67,6 +67,7 @@
67
67
  "@sveltejs/acorn-typescript": "^1.0.6",
68
68
  "acorn": "^8.15.0",
69
69
  "clsx": "^2.1.1",
70
+ "esm-env": "^1.2.2",
70
71
  "esrap": "^2.1.0",
71
72
  "is-reference": "^3.0.3",
72
73
  "magic-string": "^0.30.18",
@@ -167,9 +167,16 @@ function RipplePlugin(config) {
167
167
 
168
168
  if (code === 64) {
169
169
  // @ character
170
- // Look ahead to see if this is followed by a valid identifier character
170
+ // Look ahead to see if this is followed by a valid identifier character or opening paren
171
171
  if (this.pos + 1 < this.input.length) {
172
172
  const nextChar = this.input.charCodeAt(this.pos + 1);
173
+
174
+ // Check if this is @( for unboxing expression syntax
175
+ if (nextChar === 40) { // ( character
176
+ this.pos += 2; // skip '@('
177
+ return this.finishToken(tt.parenL, '@(');
178
+ }
179
+
173
180
  // Check if the next character can start an identifier
174
181
  if (
175
182
  (nextChar >= 65 && nextChar <= 90) || // A-Z
@@ -348,6 +355,11 @@ function RipplePlugin(config) {
348
355
  * @returns {any} Parsed expression atom
349
356
  */
350
357
  parseExprAtom(refDestructuringErrors, forNew, forInit) {
358
+ // Check if this is @(expression) for unboxing tracked values
359
+ if (this.type === tt.parenL && this.value === '@(') {
360
+ return this.parseTrackedExpression();
361
+ }
362
+
351
363
  // Check if this is a tuple literal starting with #[
352
364
  if (this.type === tt.bracketL && this.value === '#[') {
353
365
  return this.parseTrackedArrayExpression();
@@ -358,6 +370,33 @@ function RipplePlugin(config) {
358
370
  return super.parseExprAtom(refDestructuringErrors, forNew, forInit);
359
371
  }
360
372
 
373
+ /**
374
+ * Parse `@(expression)` syntax for unboxing tracked values
375
+ * Creates a TrackedExpression node with the argument property
376
+ * @returns {any} TrackedExpression node
377
+ */
378
+ parseTrackedExpression() {
379
+ const node = this.startNode();
380
+ this.next(); // consume '@(' token
381
+ node.argument = this.parseExpression();
382
+ this.expect(tt.parenR); // expect ')'
383
+ return this.finishNode(node, 'TrackedExpression');
384
+ }
385
+
386
+ /**
387
+ * Override to allow TrackedExpression as a valid lvalue for update expressions
388
+ * @param {any} expr - Expression to check
389
+ * @param {any} bindingType - Binding type
390
+ * @param {any} checkClashes - Check for clashes
391
+ */
392
+ checkLValSimple(expr, bindingType, checkClashes) {
393
+ // Allow TrackedExpression as a valid lvalue for ++/-- operators
394
+ if (expr.type === 'TrackedExpression') {
395
+ return;
396
+ }
397
+ return super.checkLValSimple(expr, bindingType, checkClashes);
398
+ }
399
+
361
400
  parseTrackedArrayExpression() {
362
401
  const node = this.startNode();
363
402
  this.next(); // consume the '#['
@@ -362,6 +362,10 @@ const visitors = {
362
362
  );
363
363
  },
364
364
 
365
+ TrackedExpression(node, context) {
366
+ return b.call('_$_.get', context.visit(node.argument));
367
+ },
368
+
365
369
  MemberExpression(node, context) {
366
370
  const parent = context.path.at(-1);
367
371
 
@@ -1001,6 +1005,16 @@ const visitors = {
1001
1005
  );
1002
1006
  }
1003
1007
 
1008
+ if (argument.type === 'TrackedExpression') {
1009
+ return b.call(
1010
+ node.prefix ? '_$_.update_pre' : '_$_.update',
1011
+ context.visit(argument.argument, { ...context.state, metadata: { tracking: null } }),
1012
+ b.id('__block'),
1013
+ node.operator === '--' ? b.literal(-1) : undefined,
1014
+ );
1015
+ }
1016
+
1017
+
1004
1018
  const left = object(argument);
1005
1019
  const binding = context.state.scope.get(left.name);
1006
1020
  const transformers = left && binding?.transform;
@@ -65,6 +65,11 @@ export interface TrackedArrayExpression extends Omit<ArrayExpression, 'type'> {
65
65
  elements: (Expression | null)[];
66
66
  }
67
67
 
68
+ export interface TrackedExpression extends Omit<Expression, 'type'> {
69
+ argument: Expression;
70
+ type: 'TrackedExpression';
71
+ }
72
+
68
73
  /**
69
74
  * Tracked object expression node
70
75
  */
@@ -1,3 +1,5 @@
1
+ import { DEV } from 'esm-env';
2
+
1
3
  export function remove_ssr_css() {
2
4
  if (!document || typeof requestAnimationFrame !== "function") {
3
5
  return;
@@ -7,14 +9,14 @@ export function remove_ssr_css() {
7
9
  }
8
10
 
9
11
  function remove_styles() {
10
- if (import.meta.env.DEV) {
12
+ if (DEV) {
11
13
  const styles = document.querySelector('style[data-vite-dev-id]');
12
14
  if (styles) {
13
15
  remove();
14
16
  } else {
15
17
  requestAnimationFrame(remove_styles);
16
18
  }
17
- } else if (import.meta.env.PROD) {
19
+ } else {
18
20
  remove_when_css_loaded(() => requestAnimationFrame(remove));
19
21
  }
20
22
  }
@@ -0,0 +1,34 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`TrackedExpression tests > should handle the syntax correctly 1`] = `
4
+ <div>
5
+ <div>
6
+ 0
7
+ </div>
8
+ <div>
9
+ 0
10
+ </div>
11
+ <div>
12
+ 1
13
+ </div>
14
+ <div>
15
+ 2
16
+ </div>
17
+ <div>
18
+ 2
19
+ </div>
20
+ <div>
21
+ 3
22
+ </div>
23
+ <div>
24
+ 4
25
+ </div>
26
+ <div>
27
+ false
28
+ </div>
29
+ <div>
30
+ true
31
+ </div>
32
+
33
+ </div>
34
+ `;
@@ -287,13 +287,13 @@ describe('compiler success tests', () => {
287
287
  component Child(props) {
288
288
  <div />
289
289
  }
290
-
290
+
291
291
  export default component App() {
292
292
  <Child data-scope="test" aria-label="accessible" class="valid" />
293
293
  }`;
294
294
 
295
295
  const result = compile(source, 'test.ripple', { mode: 'client' });
296
-
296
+
297
297
  // Should contain properly quoted hyphenated properties and unquoted valid identifiers
298
298
  expect(result.js.code).toMatch(/'data-scope': "test"/);
299
299
  expect(result.js.code).toMatch(/'aria-label': "accessible"/);
@@ -323,9 +323,9 @@ describe('compiler success tests', () => {
323
323
  component Child(props) {
324
324
  <div />
325
325
  }
326
-
326
+
327
327
  export default component App() {
328
- <Child
328
+ <Child
329
329
  validProp="valid"
330
330
  class="valid"
331
331
  id="valid"
@@ -336,12 +336,12 @@ describe('compiler success tests', () => {
336
336
  }`;
337
337
 
338
338
  const result = compile(source, 'test.ripple', { mode: 'client' });
339
-
339
+
340
340
  // Valid identifiers should not be quoted
341
341
  expect(result.js.code).toMatch(/validProp: "valid"/);
342
342
  expect(result.js.code).toMatch(/class: "valid"/);
343
343
  expect(result.js.code).toMatch(/id: "valid"/);
344
-
344
+
345
345
  // Invalid identifiers (with hyphens) should be quoted
346
346
  expect(result.js.code).toMatch(/'data-invalid': "invalid"/);
347
347
  expect(result.js.code).toMatch(/'aria-invalid': "invalid"/);
@@ -353,25 +353,113 @@ describe('compiler success tests', () => {
353
353
  component Child(props) {
354
354
  <div />
355
355
  }
356
-
356
+
357
357
  export default component App() {
358
358
  <Child data-scope="test" />
359
359
  }`;
360
360
 
361
361
  const result = compile(source, 'test.ripple', { mode: 'client' });
362
-
362
+
363
363
  // Extract the props object from the generated code and test it's valid JavaScript
364
364
  const match = result.js.code.match(/Child\([^,]+,\s*(\{[^}]+\})/);
365
365
  expect(match).toBeTruthy();
366
-
366
+
367
367
  const propsObject = match[1];
368
368
  expect(() => {
369
369
  // Test that the object literal is syntactically valid
370
370
  new Function(`return ${propsObject}`);
371
371
  }).not.toThrow();
372
-
372
+
373
373
  // Also verify it contains the expected quoted property
374
374
  expect(propsObject).toMatch(/'data-scope': "test"/);
375
375
  });
376
376
  });
377
+
378
+ describe('regex handling', () => {
379
+ it('renders without crashing using regex literals in method calls', () => {
380
+ component App() {
381
+ let text = 'Hello <span>world</span> and <div>content</div>';
382
+
383
+ // Test various regex patterns in method calls that previously failed
384
+ let matchResult = text.match(/<span>/);
385
+ let replaceResult = text.replace(/<div>/g, '[DIV]');
386
+ let searchResult = text.search(/<span>/);
387
+
388
+ // Test regex literals in variable assignments (should work)
389
+ let spanRegex = /<span>/g;
390
+ let divRegex = /<div.*?>/;
391
+
392
+ // Test more complex regex patterns
393
+ let complexMatch = text.match(/<[^>]*>/g);
394
+ let htmlTags = text.replace(/<(\/*)(\w+)[^>]*>/g, '[$1$2]');
395
+
396
+ // Test edge cases with multiple angle brackets
397
+ let multiAngle = '<<test>> <span>content</span>'.match(/<span>/);
398
+
399
+ <div>
400
+ <span>{String(matchResult)}</span>
401
+ <span>{replaceResult}</span>
402
+ <span>{String(searchResult)}</span>
403
+ <span>{String(spanRegex)}</span>
404
+ <span>{String(divRegex)}</span>
405
+ <span>{String(complexMatch)}</span>
406
+ <span>{htmlTags}</span>
407
+ <span>{String(multiAngle)}</span>
408
+ </div>
409
+ }
410
+
411
+ render(App);
412
+
413
+ const matchResult = container.querySelectorAll('span')[0];
414
+ const replaceResult = container.querySelectorAll('span')[1];
415
+ const searchResult = container.querySelectorAll('span')[2];
416
+ const spanRegex = container.querySelectorAll('span')[3];
417
+ const divRegex = container.querySelectorAll('span')[4];
418
+ const complexMatch = container.querySelectorAll('span')[5];
419
+ const htmlTags = container.querySelectorAll('span')[6];
420
+ const multiAngle = container.querySelectorAll('span')[7];
421
+
422
+ expect(matchResult.textContent).toBe('<span>');
423
+ expect(replaceResult.textContent).toBe('Hello <span>world</span> and [DIV]content</div>');
424
+ expect(searchResult.textContent).toBe('6');
425
+ expect(spanRegex.textContent).toBe('/<span>/g');
426
+ expect(divRegex.textContent).toBe('/<div.*?>/');
427
+ expect(complexMatch.textContent).toBe('<span>,</span>,<div>,</div>');
428
+ expect(htmlTags.textContent).toBe('Hello [span]world[/span] and [div]content[/div]');
429
+ expect(multiAngle.textContent).toBe('<span>');
430
+ });
431
+
432
+ it('renders without crashing mixing regex and JSX syntax', () => {
433
+ component App() {
434
+ let htmlString = '<p>Paragraph</p><div>Content</div>';
435
+
436
+ // Mix of regex parsing and legitimate JSX
437
+ let paragraphs = htmlString.match(/<p[^>]*>.*?<\/p>/g);
438
+ let cleaned = htmlString.replace(/<\/?[^>]+>/g, '');
439
+ let splitArray = htmlString.split(/<\/?\w+>/g).filter(s => s.trim());
440
+
441
+ <div class='container'>
442
+ <span class='result'>{String(paragraphs)}</span>
443
+ <span class='cleaned'>{cleaned}</span>
444
+ <p>{'This is real JSX'}</p>
445
+ <div><span>
446
+ {'Split result: '}
447
+ {splitArray.join(', ')}
448
+ </span></div>
449
+ </div>
450
+ }
451
+
452
+ render(App);
453
+
454
+ const result = container.querySelector('.result');
455
+ const cleaned = container.querySelector('.cleaned');
456
+ const jsxParagraph = container.querySelector('p');
457
+ const splitResult = container.querySelector('.container > div > span');
458
+
459
+ expect(result.textContent).toBe('<p>Paragraph</p>');
460
+ expect(cleaned.textContent).toBe('ParagraphContent');
461
+ expect(jsxParagraph.textContent).toBe('This is real JSX');
462
+ expect(splitResult.textContent).toBe('Split result: Paragraph, Content');
463
+ });
464
+ });
377
465
  });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, TrackedSet, track } from 'ripple';
3
+
4
+ describe('TrackedExpression tests', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('should handle the syntax correctly', () => {
24
+ component App() {
25
+ let count = track(0);
26
+
27
+ function get_count() {
28
+ return count;
29
+ }
30
+
31
+
32
+ <div>{@(count)}</div>
33
+ <div>{@(get_count())}</div>
34
+ <div>{++@(count)}</div>
35
+ <div>{++@(get_count())}</div>
36
+ <div>{@(count)++}</div>
37
+ <div>{@(get_count())++}</div>
38
+ <div>{@(count)}</div>
39
+ <div>{!@(count)}</div>
40
+ <div>{!!@(count)}</div>
41
+ }
42
+
43
+ render(App);
44
+ expect(container).toMatchSnapshot();
45
+ });
46
+ });