ripple 0.3.79 → 0.3.81

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # ripple
2
2
 
3
+ ## 0.3.81
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ [[`67de047`](https://github.com/Ripple-TS/ripple/commit/67de047d103f39673b25910e1a97760278820999),
9
+ [`3b6fb73`](https://github.com/Ripple-TS/ripple/commit/3b6fb73170d4ad6a383befdda951ce0da4fcbb46),
10
+ [`1c645c8`](https://github.com/Ripple-TS/ripple/commit/1c645c8f854df23bb1271b3402d1885616b525cd),
11
+ [`b1256fd`](https://github.com/Ripple-TS/ripple/commit/b1256fdb5bf279ee7dd20bf1a71dcfccc47e279c)]:
12
+ - @tsrx/core@0.1.29
13
+ - @tsrx/ripple@0.1.29
14
+
15
+ ## 0.3.80
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies
20
+ [[`f001849`](https://github.com/Ripple-TS/ripple/commit/f00184940979a77cbf6873a811caaaa436feab46),
21
+ [`4af2591`](https://github.com/Ripple-TS/ripple/commit/4af259139d118a27d177531aa6a21435a3f3a015),
22
+ [`4af2591`](https://github.com/Ripple-TS/ripple/commit/4af259139d118a27d177531aa6a21435a3f3a015),
23
+ [`87afc5d`](https://github.com/Ripple-TS/ripple/commit/87afc5d3f4c73e604cd245865e27d29e40435482),
24
+ [`87afc5d`](https://github.com/Ripple-TS/ripple/commit/87afc5d3f4c73e604cd245865e27d29e40435482),
25
+ [`f1a4c10`](https://github.com/Ripple-TS/ripple/commit/f1a4c10d2ad8ed604375f36f7ae3b653fe95ed1a),
26
+ [`87afc5d`](https://github.com/Ripple-TS/ripple/commit/87afc5d3f4c73e604cd245865e27d29e40435482)]:
27
+ - @tsrx/core@0.1.28
28
+ - @tsrx/ripple@0.1.28
29
+
3
30
  ## 0.3.79
4
31
 
5
32
  ### 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.3.79",
6
+ "version": "0.3.81",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -67,15 +67,14 @@
67
67
  "#client": "./src/runtime/internal/client/types.d.ts",
68
68
  "#client/constants": "./src/internal/client/constants.js",
69
69
  "#server": "./src/runtime/internal/server/types.d.ts",
70
- "#public": "./types/index.d.ts",
71
- "#helpers": "./src/helpers.d.ts"
70
+ "#public": "./types/index.d.ts"
72
71
  },
73
72
  "dependencies": {
74
73
  "clsx": "^2.1.1",
75
74
  "devalue": "^5.8.1",
76
75
  "esm-env": "^1.2.2",
77
- "@tsrx/core": "0.1.27",
78
- "@tsrx/ripple": "0.1.27"
76
+ "@tsrx/core": "0.1.29",
77
+ "@tsrx/ripple": "0.1.29"
79
78
  },
80
79
  "devDependencies": {
81
80
  "@types/estree": "^1.0.8",
@@ -1,5 +1,5 @@
1
1
  import type { AddEventObject, FragmentProps, RefKey, RefValue, TSRXElement } from '#public';
2
- import type { Nullable } from '#helpers';
2
+ import type { Nullable } from '@tsrx/core/types/helpers';
3
3
  export type { RefValue } from '#public';
4
4
 
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @import { Component, Dependency, Block, TryBlockWithCatch } from '#server';
3
- * @import { NestedArray } from '#helpers';
3
+ * @import { NestedArray } from '@tsrx/core/types/helpers';
4
4
  * @import { Props } from '#public';
5
5
  * @import { RenderResult, BaseRenderOptions, RenderStreamResult, Stream, StreamSink } from 'ripple/server';
6
6
  */
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { flushSync, track } from 'ripple';
3
+
4
+ describe('code blocks in template children position', () => {
5
+ it('renders a code block child with setup statements and output', () => {
6
+ function App() @{
7
+ <>
8
+ <span class="a">{'a'}</span>
9
+ @{
10
+ const x = 1;
11
+ <span class="x">{x}</span>
12
+ }
13
+ </>
14
+ }
15
+
16
+ render(App);
17
+
18
+ expect(container.querySelector('.a').textContent).toBe('a');
19
+ expect(container.querySelector('.x').textContent).toBe('1');
20
+ });
21
+
22
+ it('renders deeply nested code block children in source position', () => {
23
+ function App() @{
24
+ <>
25
+ <span class="a">{'a'}</span>
26
+ @{
27
+ const x = 1;
28
+ @{
29
+ const y = 2;
30
+ @{
31
+ <span class="sum">
32
+ {x + y}
33
+ </span>
34
+ }
35
+ }
36
+ }
37
+ <span class="b">{'b'}</span>
38
+ </>
39
+ }
40
+
41
+ render(App);
42
+
43
+ expect(container.textContent).toBe('a3b');
44
+ expect(container.querySelector('.sum').textContent).toBe('3');
45
+ });
46
+
47
+ it('renders nothing for empty nested code blocks', () => {
48
+ function App() @{
49
+ <>
50
+ <span>{'a'}</span>
51
+ <span>{'b'}</span>
52
+ @{
53
+ @{
54
+ @{}
55
+ }
56
+ }
57
+ </>
58
+ }
59
+
60
+ render(App);
61
+
62
+ expect(container.textContent).toBe('ab');
63
+ });
64
+
65
+ it('keeps code block render output reactive', () => {
66
+ function App() @{
67
+ const count = track(0);
68
+ <>
69
+ <button onClick={() => count.value++}>{'+'}</button>
70
+ @{
71
+ const doubled = () => count.value * 2;
72
+ <span class="doubled">{doubled()}</span>
73
+ }
74
+ </>
75
+ }
76
+
77
+ render(App);
78
+
79
+ expect(container.querySelector('.doubled').textContent).toBe('0');
80
+
81
+ container.querySelector('button').click();
82
+ flushSync();
83
+
84
+ expect(container.querySelector('.doubled').textContent).toBe('2');
85
+ });
86
+
87
+ it('renders a code block child inside an @if branch', () => {
88
+ function App() @{
89
+ const show = track(true);
90
+ <>
91
+ <button onClick={() => (show.value = !show.value)}>{'toggle'}</button>
92
+ @if (show.value) {
93
+ @{
94
+ const label = 'shown';
95
+ <span class="label">{label}</span>
96
+ }
97
+ }
98
+ </>
99
+ }
100
+
101
+ render(App);
102
+
103
+ expect(container.querySelector('.label').textContent).toBe('shown');
104
+
105
+ container.querySelector('button').click();
106
+ flushSync();
107
+
108
+ expect(container.querySelector('.label')).toBeNull();
109
+ });
110
+
111
+ it('gives each code block its own lexical scope', () => {
112
+ function App() @{
113
+ const y = 10;
114
+ <>
115
+ <span class="a">{'a'}</span>
116
+ @{
117
+ const x = 1;
118
+ @{
119
+ const x = 2;
120
+ @{
121
+ <span class="sum">
122
+ {x + y}
123
+ </span>
124
+ }
125
+ }
126
+ }
127
+ </>
128
+ }
129
+
130
+ render(App);
131
+
132
+ // The innermost block sees the shadowing `x = 2` and the component's `y`.
133
+ expect(container.querySelector('.sum').textContent).toBe('12');
134
+ });
135
+
136
+ it('scopes code-only block statements', () => {
137
+ function App() @{
138
+ const items: number[] = [];
139
+ <>
140
+ @{
141
+ const scoped = 1;
142
+ items.push(scoped);
143
+ }
144
+ @{
145
+ const scoped = 2;
146
+ items.push(scoped);
147
+ }
148
+ <span class="len">{items.join(',')}</span>
149
+ </>
150
+ }
151
+
152
+ render(App);
153
+
154
+ expect(container.querySelector('.len').textContent).toBe('1,2');
155
+ });
156
+
157
+ it('renders nested code block chains without extra component levels', () => {
158
+ function App() @{
159
+ <>
160
+ <span class="a">{'a'}</span>
161
+ @{
162
+ const x = 1;
163
+ @{
164
+ const y = 2;
165
+ @{
166
+ <span class="sum">
167
+ {x + y}
168
+ </span>
169
+ }
170
+ }
171
+ }
172
+ </>
173
+ }
174
+
175
+ render(App);
176
+
177
+ expect(container.textContent).toBe('a3');
178
+ expect(container.querySelector('.sum').textContent).toBe('3');
179
+ });
180
+ });
@@ -44,15 +44,15 @@ describe('compiler > basics', () => {
44
44
  const style3 = 'div {color: green }';
45
45
 
46
46
  let input = source.replace('__STYLE__', style1);
47
- let ast = parse(input);
47
+ let ast = parse(input, 'App.tsrx');
48
48
  expect(get_returned_tsrx(ast.body[0]).children.at(-1).children[0].source).toEqual(style1);
49
49
 
50
50
  input = source.replace('__STYLE__', style2);
51
- ast = parse(input);
51
+ ast = parse(input, 'App.tsrx');
52
52
  expect(get_returned_tsrx(ast.body[0]).children.at(-1).children[0].source).toEqual(style2);
53
53
 
54
54
  input = source.replace('__STYLE__', style3);
55
- ast = parse(input);
55
+ ast = parse(input, 'App.tsrx');
56
56
  expect(get_returned_tsrx(ast.body[0]).children.at(-1).children[0].source).toEqual(style3);
57
57
  });
58
58
 
@@ -67,7 +67,7 @@ describe('compiler > basics', () => {
67
67
  </>
68
68
  }`;
69
69
 
70
- const ast = parse(source);
70
+ const ast = parse(source, 'App.tsrx');
71
71
  const elements = get_returned_tsrx(ast.body[0]).children.filter(
72
72
  (node: AST.Node) => node.type === 'Element',
73
73
  ) as AST.Element[];
@@ -89,7 +89,7 @@ describe('compiler > basics', () => {
89
89
  <div>{text markup}</div>
90
90
  }`;
91
91
 
92
- expect(() => parse(invalid_source)).toThrow();
92
+ expect(() => parse(invalid_source, 'App.tsrx')).toThrow();
93
93
  });
94
94
 
95
95
  it('optimizes string-shaped expressions as text nodes', () => {
@@ -192,7 +192,7 @@ export function App() @{
192
192
  </>
193
193
  }`;
194
194
 
195
- const ast = parse(source);
195
+ const ast = parse(source, 'App.tsrx');
196
196
  const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
197
197
  const fragment = get_returned_tsrx(declaration) as any;
198
198
 
@@ -211,7 +211,7 @@ export function App() @{
211
211
  </>
212
212
  }`;
213
213
 
214
- const ast = parse(source);
214
+ const ast = parse(source, 'App.tsrx');
215
215
  const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
216
216
  const fragment = get_returned_tsrx(declaration) as any;
217
217
 
@@ -1050,7 +1050,7 @@ function App() @{
1050
1050
  }
1051
1051
  `;
1052
1052
 
1053
- const ast = parse(source);
1053
+ const ast = parse(source, 'App.tsrx');
1054
1054
  const ast_json = JSON.stringify(ast);
1055
1055
 
1056
1056
  expect(ast_json).toContain('"name":"effect"');
@@ -1223,7 +1223,7 @@ export function App() @{
1223
1223
  const source = `function TodoList({ items }: { items: { text: string }[] }) @{
1224
1224
  <ul>var a = "123"</ul>
1225
1225
  }`;
1226
- const ast = parse(source);
1226
+ const ast = parse(source, 'App.tsrx');
1227
1227
  const returned = get_returned_tsrx(ast.body[0]);
1228
1228
  const ul =
1229
1229
  returned.type === 'Element'
@@ -1262,6 +1262,6 @@ export function App() @{
1262
1262
  <4;</ul>
1263
1263
  }`;
1264
1264
 
1265
- expect(() => parse(source)).toThrow();
1265
+ expect(() => parse(source, 'App.tsrx')).toThrow();
1266
1266
  });
1267
1267
  });
@@ -270,5 +270,80 @@ export function App() @{
270
270
  expect(code).toContain('highlight');
271
271
  expect(code).toMatch(/tsrx-[a-z0-9]+ highlight/);
272
272
  });
273
+
274
+ const unreachable_selectors_source = `
275
+ export function App() @{
276
+ const styles = <style>
277
+ div {
278
+ color: red;
279
+ }
280
+ .parent .card {
281
+ font-weight: bold;
282
+ }
283
+ .card {
284
+ color: green;
285
+ &:hover {
286
+ color: blue;
287
+ }
288
+ }
289
+ :global(.badge) {
290
+ padding: 0;
291
+ }
292
+ :global(body) {
293
+ margin: 0;
294
+ }
295
+ </style>;
296
+
297
+ <div class={styles.card}>{'text'}</div>
298
+ }`;
299
+
300
+ it('prunes style expression selectors the class map cannot reach in client mode', () => {
301
+ const { css, cssHash } = compile(unreachable_selectors_source, 'test.tsrx');
302
+
303
+ expect(css).toContain('/* (unused) div {');
304
+ expect(css).toContain('/* (unused) .parent .card {');
305
+ expect(css).toContain(`.card.${cssHash}`);
306
+ expect(css).toContain('&:hover {');
307
+ expect(css).toContain('.badge {');
308
+ expect(css).not.toContain(`.badge.${cssHash}`);
309
+ expect(css).toContain('/* (unused) :global(body) {');
310
+ });
311
+
312
+ it('prunes style expression selectors the class map cannot reach in server mode', () => {
313
+ const { css, cssHash } = compile(unreachable_selectors_source, 'test.tsrx', {
314
+ mode: 'server',
315
+ });
316
+
317
+ expect(css).toContain('/* (unused) div {');
318
+ expect(css).toContain('/* (unused) .parent .card {');
319
+ expect(css).toContain(`.card.${cssHash}`);
320
+ expect(css).toContain('&:hover {');
321
+ expect(css).toContain('.badge {');
322
+ expect(css).not.toContain(`.badge.${cssHash}`);
323
+ expect(css).toContain('/* (unused) :global(body) {');
324
+ });
325
+
326
+ it('keeps all selectors of a free-standing style block', () => {
327
+ const source = `
328
+ export function App() @{
329
+ <>
330
+ <div class="card">{'text'}</div>
331
+
332
+ <style>
333
+ div {
334
+ color: red;
335
+ }
336
+ .card {
337
+ color: green;
338
+ }
339
+ </style>
340
+ </>
341
+ }`;
342
+ const { css, cssHash } = compile(source, 'test.tsrx');
343
+
344
+ expect(css).not.toContain('(unused)');
345
+ expect(css).toContain(`div.${cssHash}`);
346
+ expect(css).toContain(`.card.${cssHash}`);
347
+ });
273
348
  });
274
349
  });
@@ -0,0 +1,126 @@
1
+ describe('code blocks in template children position', () => {
2
+ it('renders a code block child with setup statements and output', async () => {
3
+ function App() @{
4
+ <>
5
+ <span class="a">{'a'}</span>
6
+ @{
7
+ const x = 1;
8
+ <span class="x">{x}</span>
9
+ }
10
+ </>
11
+ }
12
+
13
+ const { body } = await render(App);
14
+
15
+ expect(body).toBeHtml('<span class="a">a</span><span class="x">1</span>');
16
+ });
17
+
18
+ it('renders deeply nested code block children in source position', async () => {
19
+ function App() @{
20
+ <>
21
+ <span class="a">{'a'}</span>
22
+ @{
23
+ const x = 1;
24
+ @{
25
+ const y = 2;
26
+ @{
27
+ <span class="sum">
28
+ {x + y}
29
+ </span>
30
+ }
31
+ }
32
+ }
33
+ <span class="b">{'b'}</span>
34
+ </>
35
+ }
36
+
37
+ const { body } = await render(App);
38
+
39
+ expect(body).toBeHtml(
40
+ '<span class="a">a</span><span class="sum">3</span><span class="b">b</span>',
41
+ );
42
+ });
43
+
44
+ it('renders nothing for empty nested code blocks', async () => {
45
+ function App() @{
46
+ <>
47
+ <span>{'a'}</span>
48
+ <span>{'b'}</span>
49
+ @{
50
+ @{
51
+ @{}
52
+ }
53
+ }
54
+ </>
55
+ }
56
+
57
+ const { body } = await render(App);
58
+
59
+ expect(body).toBeHtml('<span>a</span><span>b</span>');
60
+ });
61
+
62
+ it('renders a code block child inside an @if branch', async () => {
63
+ function App() @{
64
+ const show = true;
65
+ <>
66
+ @if (show) {
67
+ @{
68
+ const label = 'shown';
69
+ <span class="label">{label}</span>
70
+ }
71
+ }
72
+ </>
73
+ }
74
+
75
+ const { body } = await render(App);
76
+
77
+ expect(body).toBeHtml('<span class="label">shown</span>');
78
+ });
79
+
80
+ it('gives each code block its own lexical scope', async () => {
81
+ function App() @{
82
+ const y = 10;
83
+ <>
84
+ <span class="a">{'a'}</span>
85
+ @{
86
+ const x = 1;
87
+ @{
88
+ const x = 2;
89
+ @{
90
+ <span class="sum">
91
+ {x + y}
92
+ </span>
93
+ }
94
+ }
95
+ }
96
+ </>
97
+ }
98
+
99
+ const { body } = await render(App);
100
+
101
+ expect(body).toBeHtml('<span class="a">a</span><span class="sum">12</span>');
102
+ });
103
+
104
+ it('renders nested code block chains without extra component levels', async () => {
105
+ function App() @{
106
+ <>
107
+ <span class="a">{'a'}</span>
108
+ @{
109
+ const x = 1;
110
+ @{
111
+ const y = 2;
112
+ @{
113
+ <span class="sum">
114
+ {x + y}
115
+ </span>
116
+ }
117
+ }
118
+ }
119
+ </>
120
+ }
121
+
122
+ const { body } = await render(App);
123
+
124
+ expect(body).toBeHtml('<span class="a">a</span><span class="sum">3</span>');
125
+ });
126
+ });
@@ -336,5 +336,65 @@ export function App() @{
336
336
  expect(body).toMatch(/tsrx-[a-z0-9]+/);
337
337
  expect(body).toContain('styled');
338
338
  });
339
+
340
+ it('prunes style expression selectors the class map cannot reach', () => {
341
+ const source = `
342
+ export function App() @{
343
+ const styles = <style>
344
+ div {
345
+ color: red;
346
+ }
347
+ .parent .card {
348
+ font-weight: bold;
349
+ }
350
+ .card {
351
+ color: green;
352
+ &:hover {
353
+ color: blue;
354
+ }
355
+ }
356
+ :global(.badge) {
357
+ padding: 0;
358
+ }
359
+ :global(body) {
360
+ margin: 0;
361
+ }
362
+ </style>;
363
+
364
+ <div class={styles.card}>{'text'}</div>
365
+ }`;
366
+ const { css, cssHash } = compile(source, 'test.tsrx', { mode: 'server' });
367
+
368
+ expect(css).toContain('/* (unused) div {');
369
+ expect(css).toContain('/* (unused) .parent .card {');
370
+ expect(css).toContain(`.card.${cssHash}`);
371
+ expect(css).toContain('&:hover {');
372
+ expect(css).toContain('.badge {');
373
+ expect(css).not.toContain(`.badge.${cssHash}`);
374
+ expect(css).toContain('/* (unused) :global(body) {');
375
+ });
376
+
377
+ it('keeps all selectors of a free-standing style block', () => {
378
+ const source = `
379
+ export function App() @{
380
+ <>
381
+ <div class="card">{'text'}</div>
382
+
383
+ <style>
384
+ div {
385
+ color: red;
386
+ }
387
+ .card {
388
+ color: green;
389
+ }
390
+ </style>
391
+ </>
392
+ }`;
393
+ const { css, cssHash } = compile(source, 'test.tsrx', { mode: 'server' });
394
+
395
+ expect(css).not.toContain('(unused)');
396
+ expect(css).toContain(`div.${cssHash}`);
397
+ expect(css).toContain(`.card.${cssHash}`);
398
+ });
339
399
  });
340
400
  });
@@ -20,7 +20,7 @@ function get_diagnostics(source) {
20
20
  paths: {
21
21
  '@tsrx/core/runtime/ref': ['packages/tsrx/types/runtime/ref.d.ts'],
22
22
  '@tsrx/core/types': ['packages/tsrx/types/index.d.ts'],
23
- '#helpers': ['packages/ripple/src/helpers.d.ts'],
23
+ '@tsrx/core/types/helpers': ['packages/tsrx/types/helpers.d.ts'],
24
24
  '#public': ['packages/ripple/types/index.d.ts'],
25
25
  ripple: ['packages/ripple/types/index.d.ts'],
26
26
  'ripple/jsx-runtime': ['packages/ripple/src/jsx-runtime.d.ts'],
package/src/helpers.d.ts DELETED
@@ -1,11 +0,0 @@
1
- export type RequireAllOrNone<T, K extends keyof T> =
2
- | (T & Required<Pick<T, K>>)
3
- | (T & { [P in K]?: never });
4
-
5
- export type RequiredPresent<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
6
-
7
- export type Nullable<T> = T | null;
8
-
9
- export type Nullish<T> = T | null | undefined;
10
-
11
- export type NestedArray<T> = (T | NestedArray<T>)[];