ripple 0.3.62 → 0.3.64
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 +44 -0
- package/package.json +5 -3
- package/src/jsx-runtime.d.ts +10 -9
- package/src/runtime/internal/client/expression.js +98 -4
- package/src/runtime/internal/client/template.js +45 -0
- package/src/runtime/internal/server/index.js +21 -1
- package/tests/client/basic/basic.collections.test.tsrx +120 -0
- package/tests/client/compiler/compiler.basic.test.tsrx +93 -8
- package/tests/client/tsx.test.tsrx +102 -0
- package/tests/hydration/basic.test.js +127 -0
- package/tests/hydration/compiled/client/basic.js +591 -94
- package/tests/hydration/compiled/client/events.js +9 -9
- package/tests/hydration/compiled/client/for.js +31 -31
- package/tests/hydration/compiled/client/head.js +5 -5
- package/tests/hydration/compiled/client/html.js +8 -8
- package/tests/hydration/compiled/client/if-children.js +2 -2
- package/tests/hydration/compiled/client/mixed-control-flow.js +6 -6
- package/tests/hydration/compiled/client/nested-control-flow.js +23 -23
- package/tests/hydration/compiled/client/reactivity.js +7 -7
- package/tests/hydration/compiled/client/return.js +1 -1
- package/tests/hydration/compiled/client/track-async-serialization.js +12 -12
- package/tests/hydration/compiled/client/try.js +2 -2
- package/tests/hydration/compiled/server/basic.js +504 -2
- package/tests/hydration/components/basic.tsrx +187 -0
- package/tests/server/basic.test.tsrx +143 -0
- package/tests/server/compiler.test.tsrx +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# ripple
|
|
2
2
|
|
|
3
|
+
## 0.3.64
|
|
4
|
+
|
|
5
|
+
## 0.3.63
|
|
6
|
+
|
|
7
|
+
### Patch Changes
|
|
8
|
+
|
|
9
|
+
- [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
|
|
10
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
|
|
11
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Parse nested `<tsrx>` islands
|
|
12
|
+
inside `<tsx>` expression containers as native TSRX so setup declarations and
|
|
13
|
+
references keep Volar mappings, and hydrate deeply nested `<tsx>`/`<tsrx>`
|
|
14
|
+
expression values without skipping server markers.
|
|
15
|
+
|
|
16
|
+
- [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
|
|
17
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
|
|
18
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Avoid duplicating plain text
|
|
19
|
+
when hydrating mixed TSRX collection values.
|
|
20
|
+
|
|
21
|
+
- [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
|
|
22
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
|
|
23
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Fix to_ts output for nested
|
|
24
|
+
`<tsrx>` islands inside `<tsx>` blocks.
|
|
25
|
+
|
|
26
|
+
Type JSX expression values as `TSRXElement` so IntelliSense reports assigned
|
|
27
|
+
TSX/TSRX fragments as renderable values instead of `void`.
|
|
28
|
+
|
|
29
|
+
Fix TextMate highlighting for nested `<tsrx>` and `<tsx>` tags inside JSX
|
|
30
|
+
expression containers.
|
|
31
|
+
|
|
32
|
+
- [#1153](https://github.com/Ripple-TS/ripple/pull/1153)
|
|
33
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)
|
|
34
|
+
Thanks [@leonidaz](https://github.com/leonidaz)! - Render nested `<tsx>` and
|
|
35
|
+
`<tsrx>` expression values, including arrays returned from JSX-style
|
|
36
|
+
expressions.
|
|
37
|
+
|
|
38
|
+
- Updated dependencies
|
|
39
|
+
[[`2acbbea`](https://github.com/Ripple-TS/ripple/commit/2acbbea9253ac8f516fe0d3a7a38331490e6fd8b),
|
|
40
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
|
|
41
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
|
|
42
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04),
|
|
43
|
+
[`9df9fe3`](https://github.com/Ripple-TS/ripple/commit/9df9fe3a2d26978e69172db84994ac496761cd04)]:
|
|
44
|
+
- @tsrx/core@0.1.12
|
|
45
|
+
- @tsrx/ripple@0.1.12
|
|
46
|
+
|
|
3
47
|
## 0.3.62
|
|
4
48
|
|
|
5
49
|
### 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.64",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/runtime/index-client.js",
|
|
9
9
|
"main": "src/runtime/index-client.js",
|
|
@@ -74,10 +74,12 @@
|
|
|
74
74
|
"clsx": "^2.1.1",
|
|
75
75
|
"devalue": "^5.8.1",
|
|
76
76
|
"esm-env": "^1.2.2",
|
|
77
|
-
"@tsrx/core": "0.1.
|
|
78
|
-
"@tsrx/ripple": "0.1.
|
|
77
|
+
"@tsrx/core": "0.1.12",
|
|
78
|
+
"@tsrx/ripple": "0.1.12"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
+
"@types/estree": "^1.0.8",
|
|
82
|
+
"@types/estree-jsx": "^1.0.5",
|
|
81
83
|
"@types/node": "^24.3.0"
|
|
82
84
|
}
|
|
83
85
|
}
|
package/src/jsx-runtime.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { AddEventObject } from '#public';
|
|
1
|
+
import type { AddEventObject, TSRXElement } from '#public';
|
|
2
2
|
import type { Nullable } from '#helpers';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Ripple JSX Runtime Type Definitions
|
|
6
|
-
* Ripple components are imperative
|
|
6
|
+
* Ripple components are imperative, but JSX expressions still represent
|
|
7
|
+
* renderable TSRX values when used in expression positions.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
// Ripple components don't return JSX elements - they're imperative
|
|
@@ -17,13 +18,13 @@ export function jsx(
|
|
|
17
18
|
type: string | ComponentType<any>,
|
|
18
19
|
props?: any,
|
|
19
20
|
key?: string | number | null,
|
|
20
|
-
):
|
|
21
|
+
): TSRXElement;
|
|
21
22
|
|
|
22
23
|
export function rsx(
|
|
23
24
|
type: string | ComponentType<any>,
|
|
24
25
|
props?: any,
|
|
25
26
|
key?: string | number | null,
|
|
26
|
-
):
|
|
27
|
+
): TSRXElement;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Create a JSX element with static children (optimization for multiple children)
|
|
@@ -33,13 +34,13 @@ export function jsxs(
|
|
|
33
34
|
type: string | ComponentType<any>,
|
|
34
35
|
props?: any,
|
|
35
36
|
key?: string | number | null,
|
|
36
|
-
):
|
|
37
|
+
): TSRXElement;
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* JSX Fragment component
|
|
40
|
-
*
|
|
41
|
+
* Ripple fragments are renderable expression values.
|
|
41
42
|
*/
|
|
42
|
-
export function Fragment(props: { children?: any }):
|
|
43
|
+
export function Fragment(props: { children?: any }): TSRXElement;
|
|
43
44
|
|
|
44
45
|
export type ClassValue = string | import('clsx').ClassArray | import('clsx').ClassDictionary;
|
|
45
46
|
|
|
@@ -819,8 +820,8 @@ interface SVGTextAttributes {
|
|
|
819
820
|
// Global JSX namespace for TypeScript
|
|
820
821
|
declare global {
|
|
821
822
|
namespace JSX {
|
|
822
|
-
|
|
823
|
-
type
|
|
823
|
+
type Element = TSRXElement;
|
|
824
|
+
type ElementType = keyof IntrinsicElements | ComponentType<any>;
|
|
824
825
|
|
|
825
826
|
interface IntrinsicElements {
|
|
826
827
|
// Document metadata
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
|
+
import { is_array } from '@tsrx/core/runtime/language-helpers';
|
|
3
4
|
import { branch, destroy_block, render } from './blocks.js';
|
|
4
5
|
import { BRANCH_BLOCK, UNINITIALIZED } from './constants.js';
|
|
5
6
|
import { create_text, get_next_sibling } from './operations.js';
|
|
7
|
+
import { assign_nodes } from './template.js';
|
|
6
8
|
import { active_block } from './runtime.js';
|
|
7
|
-
import { hydrating, set_hydrate_node } from './hydration.js';
|
|
9
|
+
import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
|
|
8
10
|
import { COMMENT_NODE, HYDRATION_END, HYDRATION_START, TEXT_NODE } from '../../../constants.js';
|
|
9
11
|
import { is_tsrx_element } from '../../element.js';
|
|
10
12
|
|
|
@@ -23,6 +25,93 @@ function find_enclosing_branch(block) {
|
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @param {any[]} value
|
|
30
|
+
* @param {ChildNode} anchor
|
|
31
|
+
* @param {Block} block
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function render_tsrx_collection(value, anchor, block) {
|
|
35
|
+
if (hydrating) {
|
|
36
|
+
assign_nodes(/** @type {Node} */ (hydrate_node ?? anchor), anchor);
|
|
37
|
+
render_tsrx_collection_items(value, anchor, block);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var start = document.createComment('');
|
|
42
|
+
var end = document.createComment('');
|
|
43
|
+
|
|
44
|
+
anchor.before(start, end);
|
|
45
|
+
assign_nodes(start, end);
|
|
46
|
+
render_tsrx_collection_items(value, end, block);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {any[]} value
|
|
51
|
+
* @param {ChildNode} anchor
|
|
52
|
+
* @param {Block} block
|
|
53
|
+
* @returns {void}
|
|
54
|
+
*/
|
|
55
|
+
function render_tsrx_collection_items(value, anchor, block) {
|
|
56
|
+
for (var i = 0; i < value.length; i++) {
|
|
57
|
+
var item = value[i];
|
|
58
|
+
|
|
59
|
+
if (is_tsrx_element(item)) {
|
|
60
|
+
item.render(anchor, block);
|
|
61
|
+
} else if (is_array(item)) {
|
|
62
|
+
render_tsrx_collection_items(item, anchor, block);
|
|
63
|
+
} else if (item != null) {
|
|
64
|
+
render_tsrx_collection_text(item + '', anchor);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} value
|
|
71
|
+
* @param {ChildNode} anchor
|
|
72
|
+
* @returns {void}
|
|
73
|
+
*/
|
|
74
|
+
function render_tsrx_collection_text(value, anchor) {
|
|
75
|
+
if (!hydrating) {
|
|
76
|
+
anchor.before(create_text(value));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var node = hydrate_node;
|
|
81
|
+
|
|
82
|
+
if (node?.nodeType === TEXT_NODE) {
|
|
83
|
+
var current_value = /** @type {Text} */ (node).nodeValue ?? '';
|
|
84
|
+
|
|
85
|
+
if (current_value !== value) {
|
|
86
|
+
/** @type {Text} */ (node).nodeValue = value;
|
|
87
|
+
|
|
88
|
+
if (current_value.startsWith(value)) {
|
|
89
|
+
var remaining = current_value.slice(value.length);
|
|
90
|
+
|
|
91
|
+
if (remaining !== '') {
|
|
92
|
+
var remaining_text = create_text(remaining);
|
|
93
|
+
/** @type {ChildNode} */ (node).after(remaining_text);
|
|
94
|
+
set_hydrate_node(remaining_text);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set_hydrate_node(get_next_sibling(node) ?? anchor);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var new_text = create_text(value);
|
|
105
|
+
|
|
106
|
+
if (node !== null && node !== anchor) {
|
|
107
|
+
/** @type {ChildNode} */ (node).before(new_text);
|
|
108
|
+
} else {
|
|
109
|
+
anchor.before(new_text);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
set_hydrate_node(node ?? anchor);
|
|
113
|
+
}
|
|
114
|
+
|
|
26
115
|
/**
|
|
27
116
|
* @param {Node} node
|
|
28
117
|
* @param {() => any} get_value
|
|
@@ -47,7 +136,8 @@ export function expression(node, get_value) {
|
|
|
47
136
|
|
|
48
137
|
render(() => {
|
|
49
138
|
var next_value = get_value();
|
|
50
|
-
var
|
|
139
|
+
var next_is_collection = is_array(next_value);
|
|
140
|
+
var next_is_element = next_is_collection || is_tsrx_element(next_value);
|
|
51
141
|
var is_hydration_marker = hydrating && anchor.nodeType === COMMENT_NODE;
|
|
52
142
|
|
|
53
143
|
if (is_hydration_marker) {
|
|
@@ -93,8 +183,12 @@ export function expression(node, get_value) {
|
|
|
93
183
|
var parent_branch = find_enclosing_branch(active_block);
|
|
94
184
|
|
|
95
185
|
child_block = branch(() => {
|
|
96
|
-
var block = active_block;
|
|
97
|
-
|
|
186
|
+
var block = /** @type {Block} */ (active_block);
|
|
187
|
+
if (next_is_collection) {
|
|
188
|
+
render_tsrx_collection(next_value, end ?? anchor, block);
|
|
189
|
+
} else {
|
|
190
|
+
next_value.render(end ?? anchor, block);
|
|
191
|
+
}
|
|
98
192
|
});
|
|
99
193
|
|
|
100
194
|
// Update parent branch's s.start to include content inserted before anchor.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
COMMENT_NODE,
|
|
5
|
+
HYDRATION_END,
|
|
6
|
+
HYDRATION_START,
|
|
4
7
|
TEMPLATE_FRAGMENT,
|
|
5
8
|
TEMPLATE_USE_IMPORT_NODE,
|
|
6
9
|
TEMPLATE_SVG_NAMESPACE,
|
|
@@ -177,6 +180,10 @@ export function append(anchor, dom, skip_advance) {
|
|
|
177
180
|
if (s !== null) {
|
|
178
181
|
s.end = /** @type {Node} */ (hydrate_node);
|
|
179
182
|
}
|
|
183
|
+
|
|
184
|
+
if (is_after_hydration_block(dom, hydrate_node)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
// Only advance if there's a next sibling. At the end of a component's
|
|
@@ -187,6 +194,44 @@ export function append(anchor, dom, skip_advance) {
|
|
|
187
194
|
anchor.before(/** @type {Node} */ (dom));
|
|
188
195
|
}
|
|
189
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @param {Node} start
|
|
199
|
+
* @param {Node | null} target
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
202
|
+
function is_after_hydration_block(start, target) {
|
|
203
|
+
if (
|
|
204
|
+
target === null ||
|
|
205
|
+
start.nodeType !== COMMENT_NODE ||
|
|
206
|
+
/** @type {Comment} */ (start).data !== HYDRATION_START
|
|
207
|
+
) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var current = get_next_sibling(start);
|
|
212
|
+
var depth = 0;
|
|
213
|
+
|
|
214
|
+
while (current !== null && current !== target) {
|
|
215
|
+
if (current.nodeType === COMMENT_NODE) {
|
|
216
|
+
var data = /** @type {Comment} */ (current).data;
|
|
217
|
+
|
|
218
|
+
if (data === HYDRATION_START) {
|
|
219
|
+
depth += 1;
|
|
220
|
+
} else if (data === HYDRATION_END) {
|
|
221
|
+
if (depth === 0) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
depth -= 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
current = get_next_sibling(current);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
190
235
|
export function text(data = '') {
|
|
191
236
|
if (hydrating) {
|
|
192
237
|
assign_nodes(/** @type {Node} */ (hydrate_node), /** @type {Node} */ (hydrate_node));
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from '../client/constants.js';
|
|
29
29
|
import { DEV } from 'esm-env';
|
|
30
30
|
import { is_ripple_object } from '../client/utils.js';
|
|
31
|
-
import { array_slice } from '@tsrx/core/runtime/language-helpers';
|
|
31
|
+
import { array_slice, is_array } from '@tsrx/core/runtime/language-helpers';
|
|
32
32
|
import {
|
|
33
33
|
escape,
|
|
34
34
|
escape_script,
|
|
@@ -83,6 +83,24 @@ export class TrackAsyncRunError extends Error {
|
|
|
83
83
|
|
|
84
84
|
export function noop() {}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* @param {any[]} value
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
function render_tsrx_collection(value) {
|
|
91
|
+
for (var i = 0; i < value.length; i++) {
|
|
92
|
+
var item = value[i];
|
|
93
|
+
|
|
94
|
+
if (is_tsrx_element(item)) {
|
|
95
|
+
item.render({});
|
|
96
|
+
} else if (is_array(item)) {
|
|
97
|
+
render_tsrx_collection(item);
|
|
98
|
+
} else if (item != null) {
|
|
99
|
+
output_push(escape(item));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
/**
|
|
87
105
|
* @param {any} value
|
|
88
106
|
* @returns {void}
|
|
@@ -92,6 +110,8 @@ export function render_expression(value) {
|
|
|
92
110
|
|
|
93
111
|
if (is_tsrx_element(value)) {
|
|
94
112
|
value.render({});
|
|
113
|
+
} else if (is_array(value)) {
|
|
114
|
+
render_tsrx_collection(value);
|
|
95
115
|
} else {
|
|
96
116
|
output_push(escape(value ?? ''));
|
|
97
117
|
}
|
|
@@ -2,6 +2,17 @@ import { RippleArray, flushSync, track } from 'ripple';
|
|
|
2
2
|
import { TRACKED_ARRAY } from '../../../src/runtime/internal/client/constants.js';
|
|
3
3
|
|
|
4
4
|
describe('basic client > collections', () => {
|
|
5
|
+
function countCommentNodes(node: Node) {
|
|
6
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT);
|
|
7
|
+
let count = 0;
|
|
8
|
+
|
|
9
|
+
while (walker.nextNode()) {
|
|
10
|
+
count++;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return count;
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
it('renders with simple reactive objects', () => {
|
|
6
17
|
component Basic() {
|
|
7
18
|
let &[user] = track({
|
|
@@ -106,4 +117,113 @@ describe('basic client > collections', () => {
|
|
|
106
117
|
expect(pre1.textContent).toBe('4');
|
|
107
118
|
expect(pre2.textContent).toBe('2');
|
|
108
119
|
});
|
|
120
|
+
|
|
121
|
+
it('cleans up mixed tsrx collection sentinels and trailing text on rerender', () => {
|
|
122
|
+
component App() {
|
|
123
|
+
let &[primary] = track(true);
|
|
124
|
+
|
|
125
|
+
<div class="frame">
|
|
126
|
+
{<tsx>
|
|
127
|
+
{primary
|
|
128
|
+
? [
|
|
129
|
+
'first:',
|
|
130
|
+
<strong class="middle">
|
|
131
|
+
{'one'}
|
|
132
|
+
</strong>,
|
|
133
|
+
':tail',
|
|
134
|
+
]
|
|
135
|
+
: [
|
|
136
|
+
'second:',
|
|
137
|
+
<strong class="middle">
|
|
138
|
+
{'two'}
|
|
139
|
+
</strong>,
|
|
140
|
+
':done',
|
|
141
|
+
]}
|
|
142
|
+
</tsx>}
|
|
143
|
+
</div>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => {
|
|
146
|
+
primary = !primary;
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{'toggle'}
|
|
150
|
+
</button>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
render(App);
|
|
154
|
+
|
|
155
|
+
const frame = container.querySelector('.frame');
|
|
156
|
+
const button = container.querySelector('button');
|
|
157
|
+
const initialCommentCount = countCommentNodes(frame);
|
|
158
|
+
|
|
159
|
+
expect(frame.textContent).toBe('first:one:tail');
|
|
160
|
+
expect(frame.querySelectorAll('.middle')).toHaveLength(1);
|
|
161
|
+
|
|
162
|
+
button.click();
|
|
163
|
+
flushSync();
|
|
164
|
+
|
|
165
|
+
expect(frame.textContent).toBe('second:two:done');
|
|
166
|
+
expect(frame.querySelectorAll('.middle')).toHaveLength(1);
|
|
167
|
+
expect(countCommentNodes(frame)).toBe(initialCommentCount);
|
|
168
|
+
|
|
169
|
+
button.click();
|
|
170
|
+
flushSync();
|
|
171
|
+
|
|
172
|
+
expect(frame.textContent).toBe('first:one:tail');
|
|
173
|
+
expect(frame.querySelectorAll('.middle')).toHaveLength(1);
|
|
174
|
+
expect(countCommentNodes(frame)).toBe(initialCommentCount);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('flattens nested primitive arrays inside mixed tsrx collections', () => {
|
|
178
|
+
component App() {
|
|
179
|
+
<div class="frame">
|
|
180
|
+
{<tsx>
|
|
181
|
+
{[
|
|
182
|
+
'start:',
|
|
183
|
+
['one', 2, true, null, false],
|
|
184
|
+
<strong>
|
|
185
|
+
{'!'}
|
|
186
|
+
</strong>,
|
|
187
|
+
':end',
|
|
188
|
+
]}
|
|
189
|
+
</tsx>}
|
|
190
|
+
</div>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
render(App);
|
|
194
|
+
|
|
195
|
+
expect(container.querySelector('.frame').textContent).toBe('start:one2truefalse!:end');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('flattens direct primitive array expressions', () => {
|
|
199
|
+
component App() {
|
|
200
|
+
const items = ['start:', ['one', 2], null, true, false, ':end'];
|
|
201
|
+
|
|
202
|
+
<div class="frame">{['start:', ['one', 2], null, true, false, ':end']}</div>
|
|
203
|
+
<div class="bound-frame">{items}</div>
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
render(App);
|
|
207
|
+
|
|
208
|
+
expect(container.querySelector('.frame').textContent).toBe('start:one2truefalse:end');
|
|
209
|
+
expect(container.querySelector('.bound-frame').textContent).toBe('start:one2truefalse:end');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('flattens conditional primitive array expressions', () => {
|
|
213
|
+
component App() {
|
|
214
|
+
const condition = true;
|
|
215
|
+
const ternary_items = condition
|
|
216
|
+
? ['start:', ['one', 2], null, true, false, ':end']
|
|
217
|
+
: ['fallback'];
|
|
218
|
+
const logical_items = condition && ['start:', ['one', 2], null, true, false, ':end'];
|
|
219
|
+
|
|
220
|
+
<div class="ternary-frame">{ternary_items}</div>
|
|
221
|
+
<div class="logical-frame">{logical_items}</div>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
render(App);
|
|
225
|
+
|
|
226
|
+
expect(container.querySelector('.ternary-frame').textContent).toBe('start:one2truefalse:end');
|
|
227
|
+
expect(container.querySelector('.logical-frame').textContent).toBe('start:one2truefalse:end');
|
|
228
|
+
});
|
|
109
229
|
});
|
|
@@ -350,6 +350,25 @@ const Inline = component(props) {
|
|
|
350
350
|
expect(result).not.toContain('const Inline = (__anchor, props, __block) => {');
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
it('emits function calls with nested template returns as expressions in client output', () => {
|
|
354
|
+
const source = `
|
|
355
|
+
component App() {
|
|
356
|
+
function make(flag) {
|
|
357
|
+
if (flag) {
|
|
358
|
+
return <tsx><span>{'nested'}</span></tsx>;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
<div>{make(true)}</div>
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
const result = compile(source, 'nested-template-return.tsrx', { mode: 'client' }).code;
|
|
368
|
+
|
|
369
|
+
expect(result).toContain('_$_.expression(expression, () => make(true))');
|
|
370
|
+
});
|
|
371
|
+
|
|
353
372
|
// it(
|
|
354
373
|
// 'imports and uses only obfuscated Tracked imports when encountering only shorthand syntax',
|
|
355
374
|
// () => {
|
|
@@ -460,6 +479,70 @@ component App() {
|
|
|
460
479
|
expect(track_result).not.toContain('lazy0');
|
|
461
480
|
});
|
|
462
481
|
|
|
482
|
+
it('lowers nested tsrx inside tsx in to_ts output', () => {
|
|
483
|
+
const source = `
|
|
484
|
+
component App() {
|
|
485
|
+
const content = <tsx>
|
|
486
|
+
{<tsrx>
|
|
487
|
+
const nested = <tsx>
|
|
488
|
+
<span class="nested-tsx">
|
|
489
|
+
{'inside nested tsx'}
|
|
490
|
+
</span>
|
|
491
|
+
</tsx>;
|
|
492
|
+
<div class="native">{nested}</div>
|
|
493
|
+
</tsrx>}
|
|
494
|
+
</tsx>;
|
|
495
|
+
|
|
496
|
+
{content}
|
|
497
|
+
}
|
|
498
|
+
`;
|
|
499
|
+
const result = compile_to_volar_mappings(source, 'test.tsrx').code;
|
|
500
|
+
|
|
501
|
+
expect(result).not.toContain('<tsrx>');
|
|
502
|
+
expect(result).not.toContain('</tsrx>');
|
|
503
|
+
expect(result).not.toContain('<tsx>');
|
|
504
|
+
expect(result).not.toContain('</tsx>');
|
|
505
|
+
expect(result).toContain('const nested = <>');
|
|
506
|
+
expect(result).toContain('children.push(<div class="native">');
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('maps identifiers from nested tsrx inside tsx in to_ts output', () => {
|
|
510
|
+
const source = `
|
|
511
|
+
component App() {
|
|
512
|
+
const content = <tsx>
|
|
513
|
+
{<tsrx>
|
|
514
|
+
const nested = <tsx>
|
|
515
|
+
<span class="nested-tsx">
|
|
516
|
+
{'inside nested tsx'}
|
|
517
|
+
</span>
|
|
518
|
+
</tsx>;
|
|
519
|
+
<div class="native">{nested}</div>
|
|
520
|
+
</tsrx>}
|
|
521
|
+
</tsx>;
|
|
522
|
+
|
|
523
|
+
{content}
|
|
524
|
+
}
|
|
525
|
+
`;
|
|
526
|
+
const result = compile_to_volar_mappings(source, 'test.tsrx', { loose: true });
|
|
527
|
+
const source_declaration = source.indexOf('nested =');
|
|
528
|
+
const source_reference = source.indexOf('nested}</div>');
|
|
529
|
+
const generated_declaration = result.code.indexOf('const nested') + 'const '.length;
|
|
530
|
+
const generated_reference = result.code.indexOf('nested;', generated_declaration);
|
|
531
|
+
|
|
532
|
+
function find_mapping(source_offset: number, generated_offset: number) {
|
|
533
|
+
return result.mappings.find(
|
|
534
|
+
(mapping) =>
|
|
535
|
+
mapping.sourceOffsets[0] === source_offset &&
|
|
536
|
+
mapping.generatedOffsets[0] === generated_offset &&
|
|
537
|
+
mapping.lengths[0] === 'nested'.length &&
|
|
538
|
+
mapping.generatedLengths[0] === 'nested'.length,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
expect(find_mapping(source_declaration, generated_declaration)).toBeDefined();
|
|
543
|
+
expect(find_mapping(source_reference, generated_reference)).toBeDefined();
|
|
544
|
+
});
|
|
545
|
+
|
|
463
546
|
it('preserves optional markers in to_ts TypeScript output', () => {
|
|
464
547
|
const source = `
|
|
465
548
|
export type OptionalTuple = [bar: string, baz?: string];
|
|
@@ -586,10 +669,11 @@ export function optionalFn(declRequired: string, declMaybe?: string) {
|
|
|
586
669
|
generatedOffsets: number[];
|
|
587
670
|
lengths: number[];
|
|
588
671
|
generatedLengths: number[];
|
|
589
|
-
}) =>
|
|
590
|
-
mapping.
|
|
591
|
-
|
|
592
|
-
|
|
672
|
+
}) =>
|
|
673
|
+
mapping.sourceOffsets[0] === source_offset &&
|
|
674
|
+
mapping.generatedOffsets[0] === generated_offset &&
|
|
675
|
+
mapping.lengths[0] === identifier.length &&
|
|
676
|
+
mapping.generatedLengths[0] === identifier.length,
|
|
593
677
|
);
|
|
594
678
|
|
|
595
679
|
expect(source_offset).toBeGreaterThan(-1);
|
|
@@ -887,10 +971,11 @@ component App() {
|
|
|
887
971
|
generated_length = length,
|
|
888
972
|
) {
|
|
889
973
|
return result.mappings.find(
|
|
890
|
-
(mapping) =>
|
|
891
|
-
mapping.
|
|
892
|
-
|
|
893
|
-
|
|
974
|
+
(mapping) =>
|
|
975
|
+
mapping.sourceOffsets[0] === source_offset &&
|
|
976
|
+
mapping.generatedOffsets[0] === generated_offset &&
|
|
977
|
+
mapping.lengths[0] === length &&
|
|
978
|
+
mapping.generatedLengths[0] === generated_length,
|
|
894
979
|
);
|
|
895
980
|
}
|
|
896
981
|
|