ripple 0.2.115 → 0.2.118

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 (93) hide show
  1. package/package.json +16 -16
  2. package/src/compiler/index.js +20 -1
  3. package/src/compiler/phases/1-parse/index.js +79 -0
  4. package/src/compiler/phases/3-transform/client/index.js +54 -8
  5. package/src/compiler/phases/3-transform/segments.js +107 -60
  6. package/src/compiler/phases/3-transform/server/index.js +21 -11
  7. package/src/compiler/types/index.d.ts +16 -0
  8. package/src/runtime/index-client.js +19 -185
  9. package/src/runtime/index-server.js +24 -0
  10. package/src/runtime/internal/client/bindings.js +443 -0
  11. package/src/runtime/internal/client/index.js +4 -0
  12. package/src/runtime/internal/client/runtime.js +10 -0
  13. package/src/runtime/internal/client/utils.js +0 -8
  14. package/src/runtime/map.js +11 -1
  15. package/src/runtime/set.js +11 -1
  16. package/tests/client/__snapshots__/for.test.ripple.snap +80 -0
  17. package/tests/client/_etc.test.ripple +5 -0
  18. package/tests/client/array/array.copy-within.test.ripple +120 -0
  19. package/tests/client/array/array.derived.test.ripple +495 -0
  20. package/tests/client/array/array.iteration.test.ripple +115 -0
  21. package/tests/client/array/array.mutations.test.ripple +385 -0
  22. package/tests/client/array/array.static.test.ripple +237 -0
  23. package/tests/client/array/array.to-methods.test.ripple +93 -0
  24. package/tests/client/basic/__snapshots__/basic.attributes.test.ripple.snap +60 -0
  25. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +106 -0
  26. package/tests/client/basic/__snapshots__/basic.text.test.ripple.snap +49 -0
  27. package/tests/client/basic/basic.attributes.test.ripple +474 -0
  28. package/tests/client/basic/basic.collections.test.ripple +94 -0
  29. package/tests/client/basic/basic.components.test.ripple +225 -0
  30. package/tests/client/basic/basic.errors.test.ripple +126 -0
  31. package/tests/client/basic/basic.events.test.ripple +222 -0
  32. package/tests/client/basic/basic.reactivity.test.ripple +476 -0
  33. package/tests/client/basic/basic.rendering.test.ripple +204 -0
  34. package/tests/client/basic/basic.styling.test.ripple +63 -0
  35. package/tests/client/basic/basic.utilities.test.ripple +25 -0
  36. package/tests/client/boundaries.test.ripple +2 -21
  37. package/tests/client/compiler/__snapshots__/compiler.assignments.test.ripple.snap +12 -0
  38. package/tests/client/compiler/__snapshots__/compiler.typescript.test.ripple.snap +22 -0
  39. package/tests/client/compiler/compiler.assignments.test.ripple +112 -0
  40. package/tests/client/compiler/compiler.attributes.test.ripple +95 -0
  41. package/tests/client/compiler/compiler.basic.test.ripple +203 -0
  42. package/tests/client/compiler/compiler.regex.test.ripple +87 -0
  43. package/tests/client/compiler/compiler.typescript.test.ripple +29 -0
  44. package/tests/client/{__snapshots__/composite.test.ripple.snap → composite/__snapshots__/composite.render.test.ripple.snap} +2 -2
  45. package/tests/client/composite/composite.dynamic-components.test.ripple +100 -0
  46. package/tests/client/composite/composite.generics.test.ripple +211 -0
  47. package/tests/client/composite/composite.props.test.ripple +106 -0
  48. package/tests/client/composite/composite.reactivity.test.ripple +184 -0
  49. package/tests/client/composite/composite.render.test.ripple +84 -0
  50. package/tests/client/computed-properties.test.ripple +2 -21
  51. package/tests/client/context.test.ripple +5 -22
  52. package/tests/client/date.test.ripple +1 -20
  53. package/tests/client/dynamic-elements.test.ripple +16 -24
  54. package/tests/client/for.test.ripple +4 -23
  55. package/tests/client/head.test.ripple +11 -23
  56. package/tests/client/html.test.ripple +1 -20
  57. package/tests/client/input-value.test.ripple +11 -31
  58. package/tests/client/map.test.ripple +82 -20
  59. package/tests/client/media-query.test.ripple +10 -23
  60. package/tests/client/object.test.ripple +5 -24
  61. package/tests/client/portal.test.ripple +2 -19
  62. package/tests/client/ref.test.ripple +8 -26
  63. package/tests/client/set.test.ripple +84 -22
  64. package/tests/client/svg.test.ripple +1 -22
  65. package/tests/client/switch.test.ripple +6 -25
  66. package/tests/client/tracked-expression.test.ripple +2 -21
  67. package/tests/client/typescript-generics.test.ripple +0 -21
  68. package/tests/client/url/url.derived.test.ripple +83 -0
  69. package/tests/client/url/url.parsing.test.ripple +165 -0
  70. package/tests/client/url/url.partial-removal.test.ripple +198 -0
  71. package/tests/client/url/url.reactivity.test.ripple +449 -0
  72. package/tests/client/url/url.serialization.test.ripple +50 -0
  73. package/tests/client/url-search-params/url-search-params.derived.test.ripple +84 -0
  74. package/tests/client/url-search-params/url-search-params.initialization.test.ripple +61 -0
  75. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +153 -0
  76. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +343 -0
  77. package/tests/client/url-search-params/url-search-params.retrieval.test.ripple +160 -0
  78. package/tests/client/url-search-params/url-search-params.serialization.test.ripple +53 -0
  79. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +55 -0
  80. package/tests/client.d.ts +12 -0
  81. package/tests/server/if.test.ripple +66 -0
  82. package/tests/setup-client.js +28 -0
  83. package/tsconfig.json +4 -2
  84. package/types/index.d.ts +92 -46
  85. package/LICENSE +0 -21
  86. package/tests/client/__snapshots__/basic.test.ripple.snap +0 -117
  87. package/tests/client/__snapshots__/compiler.test.ripple.snap +0 -33
  88. package/tests/client/array.test.ripple +0 -1455
  89. package/tests/client/basic.test.ripple +0 -1892
  90. package/tests/client/compiler.test.ripple +0 -541
  91. package/tests/client/composite.test.ripple +0 -692
  92. package/tests/client/url-search-params.test.ripple +0 -912
  93. package/tests/client/url.test.ripple +0 -954
@@ -1,541 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { mount, TrackedArray, track } from 'ripple';
3
- import { parse, compile } from 'ripple/compiler'
4
-
5
- describe('compiler success tests', () => {
6
- let container;
7
-
8
- function render(component) {
9
- mount(component, {
10
- target: container
11
- });
12
- }
13
-
14
- beforeEach(() => {
15
- container = document.createElement('div');
16
- document.body.appendChild(container);
17
- });
18
-
19
- afterEach(() => {
20
- document.body.removeChild(container);
21
- container = null;
22
- });
23
-
24
-
25
- it('Parses style content correctly', () => {
26
- const source = `export component App() {
27
- <div id="myid" class="myclass">{"Hello World"}</div>
28
-
29
- <style>#style</style>
30
- }`;
31
- const style1 = '.myid {color: green }';
32
- const style2 = '#myid {color: green }';
33
- const style3 = 'div {color: green }';
34
-
35
- let input = source.replace('#style', style1);
36
- let ast = parse(input);
37
- expect(ast.body[0].declaration.css.source).toEqual(style1);
38
-
39
- input = source.replace('#style', style2);
40
- ast = parse(input);
41
- expect(ast.body[0].declaration.css.source).toEqual(style2);
42
-
43
- input = source.replace('#style', style3);
44
- ast = parse(input);
45
- expect(ast.body[0].declaration.css.source).toEqual(style3);
46
- });
47
-
48
- it('renders without crashing', () => {
49
- component App() {
50
- let foo;
51
- let bar;
52
- let baz;
53
-
54
- foo = {};
55
- foo = {'test': 0};
56
- foo['abc'] = 123;
57
-
58
- bar = { 'def': 456 };
59
-
60
- baz = { 'ghi': 789 };
61
- baz['jkl'] = 987;
62
- }
63
-
64
- render(App);
65
- });
66
-
67
-
68
- it('renders without crashing using < character', () => {
69
- component App() {
70
- function bar() {
71
- for (let i = 0; i < 10; i++) {
72
- // do nothing
73
- }
74
- const x = 1 < 1;
75
- }
76
-
77
- let x = 5 < 10
78
-
79
- <div>{x}</div>
80
- }
81
-
82
- render(App);
83
- });
84
-
85
- it('render lexical blocks without crashing', () => {
86
- component App() {
87
- <div>
88
- const a = 1;
89
- <div>
90
- const b = 1;
91
- </div>
92
- <div>
93
- const b = 1;
94
- </div>
95
- </div>
96
- <div>
97
- const a = 2;
98
- <div>
99
- const b = 1;
100
- </div>
101
- </div>
102
- }
103
-
104
- render(App);
105
- });
106
-
107
- it('properly handles JS assignments, reads and updates to array indices', () => {
108
- const logs = [];
109
-
110
- component App() {
111
- let items = [];
112
- let tracked_items = track([]);
113
- let items2 = new Array();
114
- let items3 = new TrackedArray();
115
- let i = 0;
116
-
117
- logs.push(items[0]);
118
- logs.push(items[i]);
119
- logs.push(@tracked_items[0]);
120
- logs.push(@tracked_items[i]);
121
- logs.push(items2[0]);
122
- logs.push(items2[i]);
123
- logs.push(items3[0]);
124
- logs.push(items3[i]);
125
-
126
- items[0] = 123;
127
- items[i] = 123;
128
- @tracked_items[0] = 123;
129
- @tracked_items[i] = 123;
130
- items2[0] = 123;
131
- items2[i] = 123;
132
- items3[0] = 123;
133
- items3[i] = 123;
134
-
135
- logs.push(items[0]);
136
- logs.push(items[i]);
137
- logs.push(@tracked_items[0]);
138
- logs.push(@tracked_items[i]);
139
- logs.push(items2[0]);
140
- logs.push(items2[i]);
141
- logs.push(items3[0]);
142
- logs.push(items3[i]);
143
-
144
- items[0]++;
145
- items[i]++;
146
- @tracked_items[0]++;
147
- @tracked_items[i]++;
148
- items2[0]++;
149
- items2[i]++;
150
- items3[0]++;
151
- items3[i]++;
152
-
153
- logs.push(items[0]);
154
- logs.push(items[i]);
155
- logs.push(@tracked_items[0]);
156
- logs.push(@tracked_items[i]);
157
- logs.push(items2[0]);
158
- logs.push(items2[i]);
159
- logs.push(items3[0]);
160
- logs.push(items3[i]);
161
-
162
- logs.push(--items[0]);
163
- logs.push(--items[i]);
164
- logs.push(--@tracked_items[0]);
165
- logs.push(--@tracked_items[i]);
166
- logs.push(--items2[0]);
167
- logs.push(--items2[i]);
168
- logs.push(--items3[0]);
169
- logs.push(--items3[i]);
170
- }
171
-
172
- render(App);
173
-
174
- expect(logs).toEqual([
175
- undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
176
- 123, 123, 123, 123, 123, 123, 123, 123,
177
- 125, 125, 125, 125, 125, 125, 125, 125,
178
- 124, 123, 124, 123, 124, 123, 124, 123
179
- ]);
180
- });
181
-
182
- it('renders without crashing using mapped types', () => {
183
- component App() {
184
- type RecordKey = 'test';
185
- type RecordValue = { a: string, b: number };
186
-
187
- const config: Record<RecordKey, RecordValue> = {
188
- test: {
189
- a: 'test',
190
- b: 1
191
- },
192
- };
193
-
194
- const config2: { [key in RecordKey]: RecordValue } = {
195
- test: {
196
- a: 'test2',
197
- b: 2
198
- }
199
- }
200
-
201
- const config3: { [key: RecordKey]: RecordValue } = {
202
- test: {
203
- a: 'test3',
204
- b: 3
205
- }
206
- }
207
- }
208
-
209
- render(App);
210
- });
211
-
212
- it('renders without crashing using object destructuring', () => {
213
- component App() {
214
- const obj = { a: 1, b: 2, c: 3 };
215
- const { a, b, ...rest } = obj;
216
-
217
- <div>
218
- {'a '}{a} {'b '} {b} {'rest '} {JSON.stringify(rest)}
219
-
220
- <div>
221
-
222
- </div>
223
- </div>
224
- }
225
-
226
- render(App);
227
- });
228
-
229
- it('renders without crashing using object destructuring #2', () => {
230
- component App() {
231
- const obj = { a: 1, b: 2, c: 3 };
232
- const { a, b, ...rest } = obj;
233
-
234
- {'a '}{a} {'b '} {b} {'rest '} {JSON.stringify(rest)}
235
-
236
- <div>
237
-
238
- </div>
239
- }
240
-
241
- render(App);
242
- });
243
-
244
- it('should not fail with random TS syntax', () => {
245
- function tagFn() {
246
- return null;
247
- }
248
-
249
- function Wrapper() {
250
- return {
251
- unwrap: function<T>() {
252
- return null as unknown as T;
253
- }
254
- }
255
- }
256
-
257
- component App() {
258
- let x: number[] = [] as number[];
259
-
260
- const n = new Wrapper<number>().unwrap<string>();
261
-
262
- const tagResult = tagFn`value`;
263
-
264
- interface Node<T> {
265
- value: T;
266
- }
267
-
268
- class Box<T> {
269
- value: T;
270
-
271
- method<T>(): T {
272
- return this.value;
273
- }
274
- }
275
-
276
- let flag = true;
277
-
278
- const s = flag ? new Box<number>() : new Box<string>();
279
- }
280
-
281
- render(App);
282
- });
283
-
284
- describe('attribute name handling', () => {
285
- it('generates valid JavaScript for component props with hyphenated attributes', () => {
286
- const source = `
287
- component Child(props) {
288
- <div />
289
- }
290
-
291
- export default component App() {
292
- <Child data-scope="test" aria-label="accessible" class="valid" />
293
- }`;
294
-
295
- const result = compile(source, 'test.ripple', { mode: 'client' });
296
-
297
- // Should contain properly quoted hyphenated properties and unquoted valid identifiers
298
- expect(result.js.code).toMatch(/'data-scope': "test"/);
299
- expect(result.js.code).toMatch(/'aria-label': "accessible"/);
300
- expect(result.js.code).toMatch(/class: "valid"/);
301
- });
302
-
303
- it('generates valid JavaScript for all types of hyphenated attributes', () => {
304
- const testCases = [
305
- { attr: 'data-testid="value"', expected: /'data-testid': "value"/ },
306
- { attr: 'aria-label="label"', expected: /'aria-label': "label"/ },
307
- { attr: 'data-custom-attr="custom"', expected: /'data-custom-attr': "custom"/ },
308
- { attr: 'ng-if="condition"', expected: /'ng-if': "condition"/ },
309
- ];
310
-
311
- testCases.forEach(({ attr, expected }) => {
312
- const source = `
313
- component Child(props) { <div /> }
314
- export default component App() { <Child ${attr} /> }`;
315
-
316
- const result = compile(source, 'test.ripple', { mode: 'client' });
317
- expect(result.js.code).toMatch(expected);
318
- });
319
- });
320
-
321
- it('handles mixed valid and invalid attribute identifiers correctly', () => {
322
- const source = `
323
- component Child(props) {
324
- <div />
325
- }
326
-
327
- export default component App() {
328
- <Child
329
- validProp="valid"
330
- class="valid"
331
- id="valid"
332
- data-invalid="invalid"
333
- aria-invalid="invalid"
334
- custom-prop="invalid"
335
- />
336
- }`;
337
-
338
- const result = compile(source, 'test.ripple', { mode: 'client' });
339
-
340
- // Valid identifiers should not be quoted
341
- expect(result.js.code).toMatch(/validProp: "valid"/);
342
- expect(result.js.code).toMatch(/class: "valid"/);
343
- expect(result.js.code).toMatch(/id: "valid"/);
344
-
345
- // Invalid identifiers (with hyphens) should be quoted
346
- expect(result.js.code).toMatch(/'data-invalid': "invalid"/);
347
- expect(result.js.code).toMatch(/'aria-invalid': "invalid"/);
348
- expect(result.js.code).toMatch(/'custom-prop': "invalid"/);
349
- });
350
-
351
- it('ensures generated code is syntactically valid JavaScript', () => {
352
- const source = `
353
- component Child(props) {
354
- <div />
355
- }
356
-
357
- export default component App() {
358
- <Child data-scope="test" />
359
- }`;
360
-
361
- const result = compile(source, 'test.ripple', { mode: 'client' });
362
-
363
- // Extract the props object from the generated code and test it's valid JavaScript
364
- const match = result.js.code.match(/Child\([^,]+,\s*(\{[^}]+\})/);
365
- expect(match).toBeTruthy();
366
-
367
- const propsObject = match[1];
368
- expect(() => {
369
- // Test that the object literal is syntactically valid
370
- new Function(`return ${propsObject}`);
371
- }).not.toThrow();
372
-
373
- // Also verify it contains the expected quoted property
374
- expect(propsObject).toMatch(/'data-scope': "test"/);
375
- });
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
- });
465
-
466
- it('compiles tracked values in effect with assignment expression', () => {
467
- const source = `component App() {
468
- let count = track(0);
469
-
470
- effect(() => {
471
- state.count = @count;
472
- })
473
- }`;
474
- const result = compile(source, 'test.ripple');
475
- // Extract just the effect callback body
476
- const effectMatch = result.js.code.match(/effect\(\(\) => \{([^}]+)\}\)/s);
477
- expect(effectMatch[1].trim()).toMatchSnapshot();
478
- });
479
-
480
- it('compiles tracked values in effect with update expressions', () => {
481
- const source = `component App() {
482
- let count = track(5);
483
-
484
- effect(() => {
485
- untrack(() => {
486
- state.preIncrement = ++@count;
487
- state.postIncrement = @count++;
488
- state.preDecrement = --@count;
489
- state.postDecrement = @count--;
490
- });
491
- })
492
- }`;
493
- const result = compile(source, 'test.ripple');
494
- // Extract just the effect callback body
495
- const effectMatch = result.js.code.match(/effect\(\(\) => \{([\s\S]+?)\n\t\}\)\)/);
496
- expect(effectMatch[1].trim()).toMatchSnapshot();
497
- });
498
-
499
- it('compiles TSInstantiationExpression', () => {
500
- const source =
501
- `function makeBox<T>(value: T) {
502
- return { value };
503
- }
504
- const makeStringBox = makeBox<string>;
505
- const stringBox = makeStringBox('abc');
506
- const ErrorMap = Map<string, Error>;
507
- const errorMap = new ErrorMap();`;
508
-
509
- const result = compile(source, 'test.ripple', { mode: 'client' });
510
-
511
- expect(result.js.code).toMatchSnapshot();
512
- });
513
-
514
- it('removes type assertions from function parameters and leaves default values', () => {
515
- const source = `
516
- function getString(e: string = 'test') {
517
- return e;
518
- }`;
519
-
520
- const result = compile(source, 'test.ripple', { mode: 'client' });
521
-
522
- expect(result.js.code).toMatchSnapshot();
523
- });
524
-
525
- it('compiles without needing semicolons between statements and JSX', () => {
526
- const source = `export component App() {
527
- <div>const code4 = 4
528
-
529
- const code3 = 3
530
- <div>
531
- <div>
532
- const code = 1
533
- </div>
534
- const code2 = 2
535
- </div>
536
- </div>
537
- }`;
538
-
539
- const result = compile(source, 'test.ripple', { mode: 'client' });
540
- });
541
- });