ripple 0.3.43 → 0.3.45
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 +27 -0
- package/package.json +4 -4
- package/src/runtime/internal/client/events.js +5 -5
- package/src/runtime/internal/client/render.js +5 -2
- package/src/runtime/internal/server/index.js +3 -3
- package/src/runtime/reactive-value.js +8 -5
- package/tests/client/basic/basic.components.test.tsrx +7 -0
- package/tests/client/basic/basic.rendering.test.tsrx +14 -0
- package/tests/client/compiler/compiler.basic.test.tsrx +110 -0
- package/tests/client/for.test.tsrx +66 -0
- package/tests/server/basic.components.test.tsrx +7 -0
- package/tests/server/compiler.test.tsrx +38 -0
- package/tests/server/for.test.tsrx +67 -0
- package/tests/server/if.test.tsrx +13 -0
- package/types/index.d.ts +4 -11
- package/src/utils/ast.js +0 -273
- package/src/utils/attributes.js +0 -43
- package/src/utils/builders.js +0 -1287
- package/src/utils/escaping.js +0 -37
- package/src/utils/events.js +0 -154
- package/src/utils/normalize_css_property_name.js +0 -23
- package/src/utils/patterns.js +0 -24
- package/src/utils/sanitize_template_string.js +0 -7
- package/tests/utils/double-quoted-text-children.test.js +0 -16
- package/tests/utils/escaping.test.js +0 -104
- package/tests/utils/events.test.js +0 -158
- package/tests/utils/normalize_css_property_name.test.js +0 -42
- package/tests/utils/patterns.test.js +0 -386
- package/tests/utils/sanitize_template_string.test.js +0 -52
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.45
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1047](https://github.com/Ripple-TS/ripple/pull/1047)
|
|
8
|
+
[`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880)
|
|
9
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Removes duplicate utils,
|
|
10
|
+
moves most utils to @tsrx/core, include their tests.
|
|
11
|
+
|
|
12
|
+
Fixes some types
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
[[`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880),
|
|
16
|
+
[`d1acf12`](https://github.com/Ripple-TS/ripple/commit/d1acf129cdd0bf2ee596dbab26ec4df829a33880),
|
|
17
|
+
[`3928ac8`](https://github.com/Ripple-TS/ripple/commit/3928ac8816399f9eccfd40081d480042a9d74030)]:
|
|
18
|
+
- @tsrx/ripple@0.0.27
|
|
19
|
+
- ripple@0.3.45
|
|
20
|
+
|
|
21
|
+
## 0.3.44
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Updated dependencies
|
|
26
|
+
[[`f5a3c1b`](https://github.com/Ripple-TS/ripple/commit/f5a3c1b9e915c250c8cd1a7dcf4e80c44abe720f)]:
|
|
27
|
+
- @tsrx/ripple@0.0.26
|
|
28
|
+
- ripple@0.3.44
|
|
29
|
+
|
|
3
30
|
## 0.3.43
|
|
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.
|
|
6
|
+
"version": "0.3.45",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"esm-env": "^1.2.2",
|
|
77
77
|
"@types/estree": "^1.0.8",
|
|
78
78
|
"@types/estree-jsx": "^1.0.5",
|
|
79
|
-
"@tsrx/ripple": "0.0.
|
|
79
|
+
"@tsrx/ripple": "0.0.27"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/node": "^24.3.0",
|
|
@@ -84,9 +84,9 @@
|
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"@volar/language-core": "~2.4.28",
|
|
86
86
|
"vscode-languageserver-types": "^3.17.5",
|
|
87
|
-
"@tsrx/core": "0.0.
|
|
87
|
+
"@tsrx/core": "0.0.25"
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"ripple": "0.3.
|
|
90
|
+
"ripple": "0.3.45"
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* @typedef {EventTarget & Record<string, any>} DelegatedEventTarget
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
|
-
event_name_from_capture,
|
|
7
|
-
is_capture_event,
|
|
8
|
-
is_non_delegated,
|
|
9
|
-
is_passive_event,
|
|
10
|
-
} from '
|
|
6
|
+
eventNameFromCapture as event_name_from_capture,
|
|
7
|
+
isCaptureEvent as is_capture_event,
|
|
8
|
+
isNonDelegated as is_non_delegated,
|
|
9
|
+
isPassiveEvent as is_passive_event,
|
|
10
|
+
} from '@tsrx/core';
|
|
11
11
|
import {
|
|
12
12
|
active_block,
|
|
13
13
|
active_reaction,
|
|
@@ -9,10 +9,13 @@ import {
|
|
|
9
9
|
is_ripple_object,
|
|
10
10
|
} from './utils.js';
|
|
11
11
|
import { event } from './events.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getAttributeEventName as get_attribute_event_name,
|
|
14
|
+
isEventAttribute as is_event_attribute,
|
|
15
|
+
} from '@tsrx/core';
|
|
13
16
|
import { get } from './runtime.js';
|
|
14
17
|
import { clsx } from 'clsx';
|
|
15
|
-
import { normalize_css_property_name } from '
|
|
18
|
+
import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* @param {Text} text
|
|
@@ -28,10 +28,10 @@ import {
|
|
|
28
28
|
} from '../client/constants.js';
|
|
29
29
|
import { DEV } from 'esm-env';
|
|
30
30
|
import { is_ripple_object, array_slice } from '../client/utils.js';
|
|
31
|
-
import { escape, escape_script } from '
|
|
32
|
-
import { is_boolean_attribute } from '
|
|
31
|
+
import { escape, escapeScript as escape_script } from '@tsrx/core';
|
|
32
|
+
import { isBooleanAttribute as is_boolean_attribute } from '@tsrx/core';
|
|
33
33
|
import { clsx } from 'clsx';
|
|
34
|
-
import { normalize_css_property_name } from '
|
|
34
|
+
import { normalizeCssPropertyName as normalize_css_property_name } from '@tsrx/core';
|
|
35
35
|
import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../constants.js';
|
|
36
36
|
import { is_tsrx_element, normalize_children, tsrx_element } from '../../element.js';
|
|
37
37
|
import {
|
|
@@ -7,10 +7,13 @@ import { safe_scope, derived } from './internal/client/runtime.js';
|
|
|
7
7
|
* @type {new <V>(fn: () => V, start: () => void | (() => void)) => ReactiveValueT<V>}
|
|
8
8
|
*/
|
|
9
9
|
export const ReactiveValue = /** @type {any} */ (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* @template V
|
|
12
|
+
* @param {() => V} fn
|
|
13
|
+
* @param {() => void | (() => void)} start
|
|
14
|
+
* @returns {Derived}
|
|
15
|
+
*/
|
|
16
|
+
function ReactiveValue(fn, start) {
|
|
14
17
|
if (!new.target) {
|
|
15
18
|
throw new TypeError('`ReactiveValue` must be called with new');
|
|
16
19
|
}
|
|
@@ -26,7 +29,7 @@ export const ReactiveValue = /** @type {any} */ (
|
|
|
26
29
|
s();
|
|
27
30
|
return fn();
|
|
28
31
|
},
|
|
29
|
-
(
|
|
32
|
+
(_, prev) => prev,
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
);
|
|
@@ -347,6 +347,9 @@ describe('basic client > components & composition', () => {
|
|
|
347
347
|
button: component({ children }: PropsWithChildren<{}>) {
|
|
348
348
|
<button>{children}</button>
|
|
349
349
|
},
|
|
350
|
+
arrowButton: component({ children }: PropsWithChildren<{}>) => {
|
|
351
|
+
<button class="arrow-button">{children}</button>
|
|
352
|
+
},
|
|
350
353
|
};
|
|
351
354
|
|
|
352
355
|
component App() {
|
|
@@ -358,6 +361,7 @@ describe('basic client > components & composition', () => {
|
|
|
358
361
|
<h1>{'Component as Property Test'}</h1>
|
|
359
362
|
<UI.span />
|
|
360
363
|
<UI.button {children} />
|
|
364
|
+
<UI.arrowButton {children} />
|
|
361
365
|
</div>
|
|
362
366
|
}
|
|
363
367
|
|
|
@@ -367,10 +371,13 @@ describe('basic client > components & composition', () => {
|
|
|
367
371
|
const span = container.querySelector('span');
|
|
368
372
|
const button = container.querySelector('button');
|
|
369
373
|
const buttonSpan = button.querySelector('span');
|
|
374
|
+
const arrowButton = container.querySelector('.arrow-button');
|
|
375
|
+
const arrowButtonSpan = arrowButton.querySelector('span');
|
|
370
376
|
|
|
371
377
|
expect(heading.textContent).toBe('Component as Property Test');
|
|
372
378
|
expect(span.textContent).toBe('Hello from Span');
|
|
373
379
|
expect(buttonSpan.textContent).toBe('Click me!');
|
|
380
|
+
expect(arrowButtonSpan.textContent).toBe('Click me!');
|
|
374
381
|
});
|
|
375
382
|
|
|
376
383
|
it('handles empty string children', () => {
|
|
@@ -55,6 +55,20 @@ second"</pre>
|
|
|
55
55
|
expect(container.querySelector('.multiline').textContent).toBe('first\nsecond');
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it('renders bare double-quoted text in if-else branches', () => {
|
|
59
|
+
component App() {
|
|
60
|
+
let condition = false;
|
|
61
|
+
|
|
62
|
+
if (condition) {
|
|
63
|
+
"Hello Ripple"
|
|
64
|
+
} else "Hello React"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(App);
|
|
68
|
+
|
|
69
|
+
expect(container.textContent).toBe('Hello React');
|
|
70
|
+
});
|
|
71
|
+
|
|
58
72
|
it('renders dynamic text', () => {
|
|
59
73
|
component Basic() {
|
|
60
74
|
let &[message] = track('Hello World');
|
|
@@ -79,6 +79,48 @@ describe('compiler > basics', () => {
|
|
|
79
79
|
);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it('parses backtick text inside fragments as JSX text', () => {
|
|
83
|
+
const source = `let a = component () {
|
|
84
|
+
<>
|
|
85
|
+
\`333\`
|
|
86
|
+
</>
|
|
87
|
+
}`;
|
|
88
|
+
|
|
89
|
+
const ast = parse(source);
|
|
90
|
+
const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
|
|
91
|
+
const component_node = declaration.init as unknown as AST.Component;
|
|
92
|
+
const fragment = component_node.body[0] as any;
|
|
93
|
+
|
|
94
|
+
expect(fragment.type).toBe('Tsx');
|
|
95
|
+
expect(fragment.children[0].type).toBe('JSXText');
|
|
96
|
+
expect(fragment.children[0].value).toContain('`333`');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('parses backtick text around JSX elements inside fragments', () => {
|
|
100
|
+
const source = `let a = component () {
|
|
101
|
+
<>
|
|
102
|
+
\`
|
|
103
|
+
<b></b>
|
|
104
|
+
\`
|
|
105
|
+
</>
|
|
106
|
+
}`;
|
|
107
|
+
|
|
108
|
+
const ast = parse(source);
|
|
109
|
+
const declaration = (ast.body[0] as AST.VariableDeclaration).declarations[0];
|
|
110
|
+
const component_node = declaration.init as unknown as AST.Component;
|
|
111
|
+
const fragment = component_node.body[0] as any;
|
|
112
|
+
|
|
113
|
+
expect(fragment.type).toBe('Tsx');
|
|
114
|
+
expect(fragment.children.map((child: any) => child.type)).toEqual([
|
|
115
|
+
'JSXText',
|
|
116
|
+
'JSXElement',
|
|
117
|
+
'JSXText',
|
|
118
|
+
]);
|
|
119
|
+
expect(fragment.children[0].value).toContain('`');
|
|
120
|
+
expect(fragment.children[1].openingElement.name.name).toBe('b');
|
|
121
|
+
expect(fragment.children[2].value).toContain('`');
|
|
122
|
+
});
|
|
123
|
+
|
|
82
124
|
it('renders without crashing', () => {
|
|
83
125
|
component App() {
|
|
84
126
|
let foo: Record<string, number>;
|
|
@@ -282,6 +324,32 @@ describe('compiler > basics', () => {
|
|
|
282
324
|
expect(js.code).not.toMatch(/_\$_\.template\(`<!><!>`,\s*1,\s*3\)/);
|
|
283
325
|
});
|
|
284
326
|
|
|
327
|
+
it('emits anonymous component expressions as arrows in client output', () => {
|
|
328
|
+
const source = `
|
|
329
|
+
const Inline = component(props) => {
|
|
330
|
+
<div>{props.x}</div>
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
const result = compile(source, 'anonymous-component.tsrx', { mode: 'client' }).js.code;
|
|
334
|
+
|
|
335
|
+
expect(result).toContain('const Inline = (__anchor, props, __block) => {');
|
|
336
|
+
expect(result).not.toContain('function Inline');
|
|
337
|
+
expect(result).not.toContain('function (__anchor');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('emits legacy anonymous component expressions as functions in client output', () => {
|
|
341
|
+
const source = `
|
|
342
|
+
const Inline = component(props) {
|
|
343
|
+
<div>{props.x}</div>
|
|
344
|
+
}
|
|
345
|
+
`;
|
|
346
|
+
const result = compile(source, 'anonymous-component.tsrx', { mode: 'client' }).js.code;
|
|
347
|
+
|
|
348
|
+
expect(result).toContain('const Inline = function (__anchor, props, __block) {');
|
|
349
|
+
expect(result).not.toContain('function Inline');
|
|
350
|
+
expect(result).not.toContain('const Inline = (__anchor, props, __block) => {');
|
|
351
|
+
});
|
|
352
|
+
|
|
285
353
|
// it(
|
|
286
354
|
// 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
|
|
287
355
|
// () => {
|
|
@@ -426,6 +494,32 @@ export component MyComponent<Item>(props: Props<Item>) {
|
|
|
426
494
|
expect(result).toContain('export function MyComponent<Item>(props: Props<Item>)');
|
|
427
495
|
});
|
|
428
496
|
|
|
497
|
+
it('emits anonymous component expressions as arrows in to_ts output', () => {
|
|
498
|
+
const source = `
|
|
499
|
+
const Inline = component(props: { x: string }) => {
|
|
500
|
+
<div>{props.x}</div>
|
|
501
|
+
}
|
|
502
|
+
`;
|
|
503
|
+
const result = compile_to_volar_mappings(source, 'test.tsrx').code;
|
|
504
|
+
|
|
505
|
+
expect(result).toContain('const Inline = (props: { x: string }) => {');
|
|
506
|
+
expect(result).not.toContain('function Inline');
|
|
507
|
+
expect(result).not.toContain('function (props');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('emits legacy anonymous component expressions as functions in to_ts output', () => {
|
|
511
|
+
const source = `
|
|
512
|
+
const Inline = component(props: { x: string }) {
|
|
513
|
+
<div>{props.x}</div>
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
const result = compile_to_volar_mappings(source, 'test.tsrx').code;
|
|
517
|
+
|
|
518
|
+
expect(result).toContain('const Inline = function(props: { x: string }) {');
|
|
519
|
+
expect(result).not.toContain('function Inline');
|
|
520
|
+
expect(result).not.toContain('const Inline = (props: { x: string }) => {');
|
|
521
|
+
});
|
|
522
|
+
|
|
429
523
|
it('preserves generic type arguments on JSX component tags in to_ts output', () => {
|
|
430
524
|
const source = `
|
|
431
525
|
type User = { name: string };
|
|
@@ -882,6 +976,22 @@ export component App() {}
|
|
|
882
976
|
);
|
|
883
977
|
});
|
|
884
978
|
|
|
979
|
+
it('does not let nested for...of continues satisfy an outer if body', () => {
|
|
980
|
+
const source = `
|
|
981
|
+
export component App({ items }: { items: string[] }) {
|
|
982
|
+
if (items.length) {
|
|
983
|
+
for (const item of items) {
|
|
984
|
+
if (!item) continue
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}`;
|
|
988
|
+
|
|
989
|
+
const result = compile(source, 'test.tsrx', { collect: true });
|
|
990
|
+
expect(result.errors.map((error) => error.message)).toContain(
|
|
991
|
+
'Component if statements must contain a template in their "then" body. Move the if statement into an effect if it does not render anything.',
|
|
992
|
+
);
|
|
993
|
+
});
|
|
994
|
+
|
|
885
995
|
it('preserves class extends generic type arguments in volar output', () => {
|
|
886
996
|
const source = `class StringMap extends Map<string, string> {}
|
|
887
997
|
export component App() {}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RippleArray, flushSync, track } from 'ripple';
|
|
2
|
+
import { compile } from '@tsrx/ripple';
|
|
2
3
|
|
|
3
4
|
describe('for statements', () => {
|
|
4
5
|
it('renders a simple static array', () => {
|
|
@@ -15,6 +16,71 @@ describe('for statements', () => {
|
|
|
15
16
|
expect(container).toMatchSnapshot();
|
|
16
17
|
});
|
|
17
18
|
|
|
19
|
+
it('allows continue to skip an iteration', () => {
|
|
20
|
+
component App() {
|
|
21
|
+
const items = ['Item 1', '', 'Item 3'];
|
|
22
|
+
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
if (!item) continue;
|
|
25
|
+
<div class="item">{item}</div>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(App);
|
|
30
|
+
|
|
31
|
+
expect(Array.from(container.querySelectorAll('.item')).map((el) => el.textContent)).toEqual([
|
|
32
|
+
'Item 1',
|
|
33
|
+
'Item 3',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('allows continue after setup statements to skip an iteration', () => {
|
|
38
|
+
const skipped = [];
|
|
39
|
+
|
|
40
|
+
component App() {
|
|
41
|
+
const items = ['Item 1', '', 'Item 3'];
|
|
42
|
+
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
if (!item) {
|
|
45
|
+
skipped.push('skip');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
<div class="item">{item}</div>
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(App);
|
|
53
|
+
|
|
54
|
+
expect(skipped).toEqual(['skip']);
|
|
55
|
+
expect(Array.from(container.querySelectorAll('.item')).map((el) => el.textContent)).toEqual([
|
|
56
|
+
'Item 1',
|
|
57
|
+
'Item 3',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does not emit JavaScript continue in for...of skip callbacks', () => {
|
|
62
|
+
const { js } = compile(
|
|
63
|
+
`component App() {
|
|
64
|
+
const items = ['Item 1', '', 'Item 3'];
|
|
65
|
+
const skipped = [];
|
|
66
|
+
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
if (!item) {
|
|
69
|
+
skipped.push('skip');
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
<div class="item">{item}</div>
|
|
73
|
+
}
|
|
74
|
+
}`,
|
|
75
|
+
'App.tsrx',
|
|
76
|
+
{ mode: 'client' },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(js.code).toContain('skipped.push(\'skip\')');
|
|
80
|
+
expect(js.code).not.toContain('continue;');
|
|
81
|
+
expect(js.code).not.toMatch(/continue;\s*return/);
|
|
82
|
+
});
|
|
83
|
+
|
|
18
84
|
it('renders a simple dynamic array', () => {
|
|
19
85
|
component App() {
|
|
20
86
|
const items = new RippleArray('Item 1', 'Item 2', 'Item 3');
|
|
@@ -172,6 +172,9 @@ describe('basic server > components & composition', () => {
|
|
|
172
172
|
button: component({ children }: PropsWithChildren<{}>) {
|
|
173
173
|
<button>{children}</button>
|
|
174
174
|
},
|
|
175
|
+
arrowButton: component({ children }: PropsWithChildren<{}>) => {
|
|
176
|
+
<button class="arrow-button">{children}</button>
|
|
177
|
+
},
|
|
175
178
|
};
|
|
176
179
|
|
|
177
180
|
component App() {
|
|
@@ -183,6 +186,7 @@ describe('basic server > components & composition', () => {
|
|
|
183
186
|
<h1>{'Component as Property Test'}</h1>
|
|
184
187
|
<UI.span />
|
|
185
188
|
<UI.button {children} />
|
|
189
|
+
<UI.arrowButton {children} />
|
|
186
190
|
</div>
|
|
187
191
|
}
|
|
188
192
|
|
|
@@ -193,10 +197,13 @@ describe('basic server > components & composition', () => {
|
|
|
193
197
|
const span = document.querySelector('span');
|
|
194
198
|
const button = document.querySelector('button');
|
|
195
199
|
const buttonSpan = button.querySelector('span');
|
|
200
|
+
const arrowButton = document.querySelector('.arrow-button');
|
|
201
|
+
const arrowButtonSpan = arrowButton.querySelector('span');
|
|
196
202
|
|
|
197
203
|
expect(heading.textContent).toBe('Component as Property Test');
|
|
198
204
|
expect(span.textContent).toBe('Hello from Span');
|
|
199
205
|
expect(buttonSpan.textContent).toBe('Click me!');
|
|
206
|
+
expect(arrowButtonSpan.textContent).toBe('Click me!');
|
|
200
207
|
});
|
|
201
208
|
|
|
202
209
|
it('handles empty string children', async () => {
|
|
@@ -87,6 +87,44 @@ export default component A() {
|
|
|
87
87
|
expect(result).not.toContain(`"Hello";`);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it('decodes JSX-style entities before server text escaping', () => {
|
|
91
|
+
const result = compile(
|
|
92
|
+
`component App() {
|
|
93
|
+
<div>"Rock & "Roll""</div>
|
|
94
|
+
}`,
|
|
95
|
+
'/src/App.tsrx',
|
|
96
|
+
{ mode: 'server' },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(result.js.code).toContain(`_$_.output_push('Rock & "Roll"')`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('emits anonymous component expressions as arrows in SSR output', () => {
|
|
103
|
+
const source = `
|
|
104
|
+
const Inline = component(props) => {
|
|
105
|
+
<div>{props.x}</div>
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
const result = compile(source, 'anonymous-component.tsrx', { mode: 'server' }).js.code;
|
|
109
|
+
|
|
110
|
+
expect(result).toContain('const Inline = (props) => {');
|
|
111
|
+
expect(result).not.toContain('function Inline');
|
|
112
|
+
expect(result).not.toContain('function (props');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('emits legacy anonymous component expressions as functions in SSR output', () => {
|
|
116
|
+
const source = `
|
|
117
|
+
const Inline = component(props) {
|
|
118
|
+
<div>{props.x}</div>
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
const result = compile(source, 'anonymous-component.tsrx', { mode: 'server' }).js.code;
|
|
122
|
+
|
|
123
|
+
expect(result).toContain('const Inline = function (props) {');
|
|
124
|
+
expect(result).not.toContain('function Inline');
|
|
125
|
+
expect(result).not.toContain('const Inline = (props) => {');
|
|
126
|
+
});
|
|
127
|
+
|
|
90
128
|
it('throws error for calling children as a function in SSR mode', () => {
|
|
91
129
|
const source = `
|
|
92
130
|
export component Layout({ children }) {
|
|
@@ -95,4 +95,71 @@ describe('for statements in SSR', () => {
|
|
|
95
95
|
'<div class="Item 1 0">Item 1 0</div><div class="Item 2 1">Item 2 1</div><div class="Item 3 2">Item 3 2</div>',
|
|
96
96
|
);
|
|
97
97
|
});
|
|
98
|
+
|
|
99
|
+
it('allows continue to skip a for...of iteration', async () => {
|
|
100
|
+
component App() {
|
|
101
|
+
const items = ['Item 1', '', 'Item 3'];
|
|
102
|
+
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
if (!item) continue;
|
|
105
|
+
<div>{item}</div>
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { body } = await render(App);
|
|
110
|
+
expect(body).toBeHtml('<div>Item 1</div><div>Item 3</div>');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('allows ordinary function control flow inside for...of loops', async () => {
|
|
114
|
+
component App() {
|
|
115
|
+
const items = ['Item 1', '', 'Item 3'];
|
|
116
|
+
|
|
117
|
+
for (const item of items) {
|
|
118
|
+
function label(value) {
|
|
119
|
+
for (let i = 0; i < 1; i++) {
|
|
120
|
+
while (i < 0) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (!value) return 'missing';
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
<div>{label(item)}</div>
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { body } = await render(App);
|
|
133
|
+
expect(body).toBeHtml('<div>Item 1</div><div>missing</div><div>Item 3</div>');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('throws for return statements inside for...of loops', () => {
|
|
137
|
+
expect(
|
|
138
|
+
() => compile(
|
|
139
|
+
`component App(items) {
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
if (!item) return
|
|
142
|
+
<div>{item}</div>
|
|
143
|
+
}
|
|
144
|
+
}`,
|
|
145
|
+
'App.tsrx',
|
|
146
|
+
{ mode: 'server' },
|
|
147
|
+
),
|
|
148
|
+
).toThrow('Return statements are not allowed inside component for...of loops');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('throws for break statements targeting for...of loops', () => {
|
|
152
|
+
expect(
|
|
153
|
+
() => compile(
|
|
154
|
+
`component App(items) {
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
if (!item) break
|
|
157
|
+
<div>{item}</div>
|
|
158
|
+
}
|
|
159
|
+
}`,
|
|
160
|
+
'App.tsrx',
|
|
161
|
+
{ mode: 'server' },
|
|
162
|
+
),
|
|
163
|
+
).toThrow('Break statements are not allowed inside component for...of loops');
|
|
164
|
+
});
|
|
98
165
|
});
|
|
@@ -27,6 +27,19 @@ describe('if statements in SSR', () => {
|
|
|
27
27
|
expect(body).toBeHtml('<div>Else block</div>');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it('renders bare double-quoted text in if-else branches', async () => {
|
|
31
|
+
component App() {
|
|
32
|
+
let condition = false;
|
|
33
|
+
|
|
34
|
+
if (condition) {
|
|
35
|
+
"Hello Ripple"
|
|
36
|
+
} else "Hello React"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { body } = await render(App);
|
|
40
|
+
expect(body).toBeHtml('Hello React');
|
|
41
|
+
});
|
|
42
|
+
|
|
30
43
|
it('renders else if block when condition is true', async () => {
|
|
31
44
|
component App() {
|
|
32
45
|
let value = 'b';
|
package/types/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { ExtendedEventOptions } from '@tsrx/core/types';
|
|
2
|
+
export type { AddEventOptions, AddEventObject, ExtendedEventOptions } from '@tsrx/core/types';
|
|
3
|
+
|
|
1
4
|
export type Component<T = Record<string, any>> = (props: T) => void;
|
|
2
5
|
|
|
3
6
|
declare const TSRX_ELEMENT: unique symbol;
|
|
@@ -201,16 +204,6 @@ export function trackPending<V>(value: Tracked<V> | (() => any)): boolean;
|
|
|
201
204
|
|
|
202
205
|
export function peek<V>(tracked: Tracked<V>): V;
|
|
203
206
|
|
|
204
|
-
export interface AddEventOptions extends ExtendedEventOptions {
|
|
205
|
-
customName?: string;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export interface AddEventObject extends AddEventOptions, EventListenerObject {}
|
|
209
|
-
|
|
210
|
-
export interface ExtendedEventOptions extends AddEventListenerOptions, EventListenerOptions {
|
|
211
|
-
delegated?: boolean;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
207
|
export type OnEventListenerRemover = () => void;
|
|
215
208
|
|
|
216
209
|
export function on<Type extends keyof WindowEventMap>(
|
|
@@ -364,7 +357,7 @@ export const RippleURL: RippleURLConstructor;
|
|
|
364
357
|
export function createSubscriber(start: () => void | (() => void)): () => void;
|
|
365
358
|
|
|
366
359
|
declare const REACTIVE_VALUE_BRAND: unique symbol;
|
|
367
|
-
interface ReactiveValue<V> extends Tracked<V> {
|
|
360
|
+
export interface ReactiveValue<V> extends Tracked<V> {
|
|
368
361
|
new (fn: () => Tracked<V>, start: () => void | (() => void)): Tracked<V>;
|
|
369
362
|
[REACTIVE_VALUE_BRAND]: void;
|
|
370
363
|
}
|