ripple 0.3.10 → 0.3.11
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 +31 -0
- package/package.json +2 -2
- package/src/compiler/errors.js +1 -1
- package/src/compiler/index.d.ts +3 -1
- package/src/compiler/phases/1-parse/index.js +170 -8
- package/src/compiler/phases/2-analyze/index.js +231 -20
- package/src/compiler/phases/3-transform/client/index.js +169 -77
- package/src/compiler/phases/3-transform/server/index.js +46 -3
- package/src/compiler/types/index.d.ts +19 -2
- package/src/compiler/types/parse.d.ts +1 -1
- package/src/compiler/utils.js +174 -0
- package/src/runtime/index-client.js +14 -4
- package/src/runtime/internal/client/composite.js +2 -2
- package/src/runtime/internal/client/expression.js +64 -2
- package/src/runtime/internal/client/portal.js +1 -1
- package/src/utils/builders.js +30 -0
- package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
- package/tests/client/basic/basic.rendering.test.ripple +4 -2
- package/tests/client/composite/composite.render.test.ripple +46 -0
- package/tests/client/return.test.ripple +101 -0
- package/tests/client/tsx.test.ripple +486 -0
- package/tests/hydration/compiled/client/basic.js +8 -24
- package/tests/hydration/compiled/client/composite.js +6 -24
- package/tests/hydration/compiled/client/events.js +9 -54
- package/tests/hydration/compiled/client/for.js +59 -152
- package/tests/hydration/compiled/client/head.js +5 -20
- package/tests/hydration/compiled/client/hmr.js +2 -8
- package/tests/hydration/compiled/client/html.js +59 -226
- package/tests/hydration/compiled/client/if-children.js +6 -22
- package/tests/hydration/compiled/client/mixed-control-flow.js +18 -66
- package/tests/hydration/compiled/client/nested-control-flow.js +92 -368
- package/tests/hydration/compiled/client/portal.js +4 -16
- package/tests/hydration/compiled/client/reactivity.js +7 -40
- package/tests/hydration/compiled/client/return.js +1 -4
- package/tests/hydration/compiled/client/try.js +2 -2
- package/tests/utils/compiler-compat-config.test.js +38 -0
- package/tests/utils/vite-plugin-config.test.js +113 -0
- package/tsconfig.typecheck.json +2 -1
- package/types/index.d.ts +2 -12
package/src/compiler/utils.js
CHANGED
|
@@ -1087,3 +1087,177 @@ export function get_ripple_namespace_call_name(name) {
|
|
|
1087
1087
|
export function ripple_import_requires_block(name) {
|
|
1088
1088
|
return name !== 'effect' && name !== 'untrack' && name !== 'Context';
|
|
1089
1089
|
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Converts a JSXMemberExpression to an AST MemberExpression.
|
|
1093
|
+
* e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
|
|
1094
|
+
* @param {import('estree-jsx').JSXMemberExpression} jsx_member
|
|
1095
|
+
* @returns {AST.MemberExpression}
|
|
1096
|
+
*/
|
|
1097
|
+
function jsx_member_expression_to_member_expression(jsx_member) {
|
|
1098
|
+
/** @type {AST.Expression} */
|
|
1099
|
+
let object;
|
|
1100
|
+
|
|
1101
|
+
if (jsx_member.object.type === 'JSXMemberExpression') {
|
|
1102
|
+
// Recursively convert nested member expressions
|
|
1103
|
+
object = jsx_member_expression_to_member_expression(jsx_member.object);
|
|
1104
|
+
} else {
|
|
1105
|
+
// Base case: JSXIdentifier
|
|
1106
|
+
object = /** @type {AST.Identifier} */ ({
|
|
1107
|
+
type: 'Identifier',
|
|
1108
|
+
name: jsx_member.object.name,
|
|
1109
|
+
start: jsx_member.object.start,
|
|
1110
|
+
end: jsx_member.object.end,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return /** @type {AST.MemberExpression} */ ({
|
|
1115
|
+
type: 'MemberExpression',
|
|
1116
|
+
object,
|
|
1117
|
+
property: /** @type {AST.Identifier} */ ({
|
|
1118
|
+
type: 'Identifier',
|
|
1119
|
+
name: jsx_member.property.name,
|
|
1120
|
+
start: jsx_member.property.start,
|
|
1121
|
+
end: jsx_member.property.end,
|
|
1122
|
+
}),
|
|
1123
|
+
computed: false,
|
|
1124
|
+
optional: false,
|
|
1125
|
+
start: jsx_member.start,
|
|
1126
|
+
end: jsx_member.end,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Converts a JSX AST node (JSXElement, JSXText, etc.) to a Ripple AST node
|
|
1132
|
+
* (Element, Text, RippleExpression) for processing inside `<tsx>` blocks.
|
|
1133
|
+
* @param {AST.Node} node
|
|
1134
|
+
* @returns {AST.Node | AST.Node[] | null}
|
|
1135
|
+
*/
|
|
1136
|
+
export function jsx_to_ripple_node(node) {
|
|
1137
|
+
if (node.type === 'JSXElement') {
|
|
1138
|
+
const opening = node.openingElement;
|
|
1139
|
+
const name = opening.name;
|
|
1140
|
+
|
|
1141
|
+
/** @type {AST.Identifier | AST.MemberExpression} */
|
|
1142
|
+
let id;
|
|
1143
|
+
|
|
1144
|
+
if (name.type === 'JSXIdentifier') {
|
|
1145
|
+
id = /** @type {AST.Identifier} */ ({
|
|
1146
|
+
type: 'Identifier',
|
|
1147
|
+
name: name.name,
|
|
1148
|
+
start: name.start,
|
|
1149
|
+
end: name.end,
|
|
1150
|
+
});
|
|
1151
|
+
} else if (name.type === 'JSXMemberExpression') {
|
|
1152
|
+
// Convert JSXMemberExpression to MemberExpression
|
|
1153
|
+
// e.g., <Foo.Bar.Baz> → MemberExpression(MemberExpression(Foo, Bar), Baz)
|
|
1154
|
+
id = jsx_member_expression_to_member_expression(name);
|
|
1155
|
+
} else if (name.type === 'JSXNamespacedName') {
|
|
1156
|
+
// For JSXNamespacedName like <namespace:element>, create an identifier with the full name
|
|
1157
|
+
id = /** @type {AST.Identifier} */ ({
|
|
1158
|
+
type: 'Identifier',
|
|
1159
|
+
name: name.namespace.name + ':' + name.name.name,
|
|
1160
|
+
start: name.start,
|
|
1161
|
+
end: name.end,
|
|
1162
|
+
});
|
|
1163
|
+
} else {
|
|
1164
|
+
// Fallback - should not reach here
|
|
1165
|
+
id = /** @type {AST.Identifier} */ ({
|
|
1166
|
+
type: 'Identifier',
|
|
1167
|
+
name: 'unknown',
|
|
1168
|
+
start: /** @type {any} */ (name).start,
|
|
1169
|
+
end: /** @type {any} */ (name).end,
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const attributes = opening.attributes
|
|
1174
|
+
.map((attr) => {
|
|
1175
|
+
if (attr.type === 'JSXAttribute') {
|
|
1176
|
+
const is_dynamic = attr.value && attr.value.type === 'JSXExpressionContainer';
|
|
1177
|
+
return /** @type {AST.Node} */ ({
|
|
1178
|
+
type: 'Attribute',
|
|
1179
|
+
name: {
|
|
1180
|
+
type: 'Identifier',
|
|
1181
|
+
name:
|
|
1182
|
+
attr.name.type === 'JSXIdentifier'
|
|
1183
|
+
? attr.name.name
|
|
1184
|
+
: attr.name.namespace.name + ':' + attr.name.name.name,
|
|
1185
|
+
tracked: is_dynamic,
|
|
1186
|
+
start: attr.name.start,
|
|
1187
|
+
end: attr.name.end,
|
|
1188
|
+
},
|
|
1189
|
+
value: attr.value
|
|
1190
|
+
? attr.value.type === 'JSXExpressionContainer'
|
|
1191
|
+
? attr.value.expression
|
|
1192
|
+
: attr.value
|
|
1193
|
+
: null,
|
|
1194
|
+
shorthand: false,
|
|
1195
|
+
start: attr.start,
|
|
1196
|
+
end: attr.end,
|
|
1197
|
+
});
|
|
1198
|
+
} else if (attr.type === 'JSXSpreadAttribute') {
|
|
1199
|
+
return /** @type {AST.Node} */ ({
|
|
1200
|
+
type: 'SpreadAttribute',
|
|
1201
|
+
argument: attr.argument,
|
|
1202
|
+
start: attr.start,
|
|
1203
|
+
end: attr.end,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
})
|
|
1208
|
+
.filter(Boolean);
|
|
1209
|
+
|
|
1210
|
+
const children = /** @type {AST.Node[]} */ (
|
|
1211
|
+
/** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
return /** @type {AST.Element} */ (
|
|
1215
|
+
/** @type {unknown} */ ({
|
|
1216
|
+
type: 'Element',
|
|
1217
|
+
id,
|
|
1218
|
+
attributes,
|
|
1219
|
+
children,
|
|
1220
|
+
selfClosing: opening.selfClosing,
|
|
1221
|
+
metadata: { scoped: false, path: /** @type {string[]} */ ([]) },
|
|
1222
|
+
start: node.start,
|
|
1223
|
+
end: node.end,
|
|
1224
|
+
})
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (node.type === 'JSXText') {
|
|
1229
|
+
if (node.value.trim() === '') return null;
|
|
1230
|
+
return /** @type {AST.Node} */ ({
|
|
1231
|
+
type: 'Text',
|
|
1232
|
+
expression: {
|
|
1233
|
+
type: 'Literal',
|
|
1234
|
+
value: node.value,
|
|
1235
|
+
raw: JSON.stringify(node.value),
|
|
1236
|
+
start: node.start,
|
|
1237
|
+
end: node.end,
|
|
1238
|
+
},
|
|
1239
|
+
metadata: {},
|
|
1240
|
+
start: node.start,
|
|
1241
|
+
end: node.end,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (node.type === 'JSXExpressionContainer') {
|
|
1246
|
+
if (node.expression.type === 'JSXEmptyExpression') return null;
|
|
1247
|
+
return /** @type {AST.Node} */ ({
|
|
1248
|
+
type: 'RippleExpression',
|
|
1249
|
+
expression: node.expression,
|
|
1250
|
+
metadata: {},
|
|
1251
|
+
start: node.start,
|
|
1252
|
+
end: node.end,
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (node.type === 'JSXFragment') {
|
|
1257
|
+
return /** @type {AST.Node[]} */ (
|
|
1258
|
+
/** @type {AST.Node[]} */ (node.children).map(jsx_to_ripple_node).flat().filter(Boolean)
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return node;
|
|
1263
|
+
}
|
|
@@ -22,15 +22,24 @@ import { COMMENT_NODE, HYDRATION_START } from '../constants.js';
|
|
|
22
22
|
// Re-export JSX runtime functions for jsxImportSource: "ripple"
|
|
23
23
|
export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* @returns {CompatOptions | undefined}
|
|
27
|
+
*/
|
|
28
|
+
function get_default_compat() {
|
|
29
|
+
return /** @type {typeof globalThis & { __RIPPLE_COMPAT__?: CompatOptions }} */ (globalThis)
|
|
30
|
+
.__RIPPLE_COMPAT__;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* @param {(anchor: Node, props: Record<string, any>, active_block: Block | null) => void} component
|
|
27
|
-
* @param {{ props?: Record<string, any>, target: HTMLElement
|
|
35
|
+
* @param {{ props?: Record<string, any>, target: HTMLElement }} options
|
|
28
36
|
* @returns {() => void}
|
|
29
37
|
*/
|
|
30
38
|
export function mount(component, options) {
|
|
31
39
|
init_operations();
|
|
32
40
|
remove_ssr_css();
|
|
33
41
|
|
|
42
|
+
const compat = get_default_compat();
|
|
34
43
|
const props = options.props || {};
|
|
35
44
|
const target = options.target;
|
|
36
45
|
const anchor = create_anchor();
|
|
@@ -46,7 +55,7 @@ export function mount(component, options) {
|
|
|
46
55
|
|
|
47
56
|
const _root = root(() => {
|
|
48
57
|
component(anchor, props, active_block);
|
|
49
|
-
},
|
|
58
|
+
}, compat);
|
|
50
59
|
|
|
51
60
|
return () => {
|
|
52
61
|
cleanup_events();
|
|
@@ -56,13 +65,14 @@ export function mount(component, options) {
|
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* @param {(anchor: Node, props: Record<string, any>, active_block: Block | null) => void} component
|
|
59
|
-
* @param {{ props?: Record<string, any>, target: HTMLElement
|
|
68
|
+
* @param {{ props?: Record<string, any>, target: HTMLElement }} options
|
|
60
69
|
* @returns {() => void}
|
|
61
70
|
*/
|
|
62
71
|
export function hydrate(component, options) {
|
|
63
72
|
init_operations();
|
|
64
73
|
remove_ssr_css();
|
|
65
74
|
|
|
75
|
+
const compat = get_default_compat();
|
|
66
76
|
const props = options.props || {};
|
|
67
77
|
const target = options.target;
|
|
68
78
|
const was_hydrating = hydrating;
|
|
@@ -86,7 +96,7 @@ export function hydrate(component, options) {
|
|
|
86
96
|
|
|
87
97
|
_root = root(() => {
|
|
88
98
|
component(/** @type {Comment} */ (anchor), props, active_block);
|
|
89
|
-
},
|
|
99
|
+
}, compat);
|
|
90
100
|
} catch (e) {
|
|
91
101
|
throw e;
|
|
92
102
|
} finally {
|
|
@@ -74,9 +74,9 @@ export function composite(get_component, node, props) {
|
|
|
74
74
|
element.appendChild(child_anchor);
|
|
75
75
|
|
|
76
76
|
if (ns !== DEFAULT_NAMESPACE) {
|
|
77
|
-
with_ns(ns, () => props.children.render(child_anchor,
|
|
77
|
+
with_ns(ns, () => props.children.render(child_anchor, block));
|
|
78
78
|
} else {
|
|
79
|
-
props.children.render(child_anchor,
|
|
79
|
+
props.children.render(child_anchor, block);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
};
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
/** @import { Block } from '#client' */
|
|
2
2
|
|
|
3
3
|
import { branch, destroy_block, render } from './blocks.js';
|
|
4
|
-
import { UNINITIALIZED } from './constants.js';
|
|
4
|
+
import { BRANCH_BLOCK, UNINITIALIZED } from './constants.js';
|
|
5
5
|
import { create_text, get_next_sibling } from './operations.js';
|
|
6
6
|
import { active_block } from './runtime.js';
|
|
7
7
|
import { hydrating, set_hydrate_node } from './hydration.js';
|
|
8
8
|
import { COMMENT_NODE, HYDRATION_END, HYDRATION_START, TEXT_NODE } from '../../../constants.js';
|
|
9
9
|
import { is_ripple_element } from '../../element.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Finds the nearest enclosing BRANCH_BLOCK in the block hierarchy.
|
|
13
|
+
* @param {Block | null} block
|
|
14
|
+
* @returns {Block | null}
|
|
15
|
+
*/
|
|
16
|
+
function find_enclosing_branch(block) {
|
|
17
|
+
while (block !== null) {
|
|
18
|
+
if ((block.f & BRANCH_BLOCK) !== 0) {
|
|
19
|
+
return block;
|
|
20
|
+
}
|
|
21
|
+
block = block.p;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
/**
|
|
12
27
|
* @param {Node} node
|
|
13
28
|
* @param {() => any} get_value
|
|
@@ -25,6 +40,10 @@ export function expression(node, get_value) {
|
|
|
25
40
|
var value = UNINITIALIZED;
|
|
26
41
|
var is_element = false;
|
|
27
42
|
var initialized = false;
|
|
43
|
+
/** @type {Block | null} */
|
|
44
|
+
var modified_parent_branch = null;
|
|
45
|
+
/** @type {Node | null} */
|
|
46
|
+
var original_parent_start = null;
|
|
28
47
|
|
|
29
48
|
render(() => {
|
|
30
49
|
var next_value = get_value();
|
|
@@ -53,6 +72,12 @@ export function expression(node, get_value) {
|
|
|
53
72
|
if (child_block !== null) {
|
|
54
73
|
destroy_block(child_block);
|
|
55
74
|
child_block = null;
|
|
75
|
+
// Restore parent branch's start since we may update it again below
|
|
76
|
+
if (modified_parent_branch !== null && modified_parent_branch.s !== null) {
|
|
77
|
+
modified_parent_branch.s.start = original_parent_start;
|
|
78
|
+
modified_parent_branch = null;
|
|
79
|
+
original_parent_start = null;
|
|
80
|
+
}
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
if (end !== null && (initialized || !hydrating)) {
|
|
@@ -63,11 +88,41 @@ export function expression(node, get_value) {
|
|
|
63
88
|
set_hydrate_node(get_next_sibling(anchor) ?? end);
|
|
64
89
|
}
|
|
65
90
|
|
|
91
|
+
// Find the enclosing branch block BEFORE creating child_block
|
|
92
|
+
// so we can update its s.start to include content inserted before anchor
|
|
93
|
+
var parent_branch = find_enclosing_branch(active_block);
|
|
94
|
+
|
|
66
95
|
child_block = branch(() => {
|
|
67
96
|
var block = active_block;
|
|
68
|
-
next_value.render(end ?? anchor,
|
|
97
|
+
next_value.render(end ?? anchor, block);
|
|
69
98
|
});
|
|
70
99
|
|
|
100
|
+
// Update parent branch's s.start to include content inserted before anchor.
|
|
101
|
+
// This ensures that when the parent branch is destroyed, the full DOM range
|
|
102
|
+
// (including RippleElement content) is removed.
|
|
103
|
+
if (
|
|
104
|
+
parent_branch !== null &&
|
|
105
|
+
parent_branch.s !== null &&
|
|
106
|
+
child_block.s !== null &&
|
|
107
|
+
child_block.s.start !== null
|
|
108
|
+
) {
|
|
109
|
+
// The child inserted content before the anchor. Update parent's start
|
|
110
|
+
// to encompass this content.
|
|
111
|
+
var child_start = child_block.s.start;
|
|
112
|
+
var parent_start = parent_branch.s.start;
|
|
113
|
+
|
|
114
|
+
// If parent's start is the anchor (or comes after child's start),
|
|
115
|
+
// update it to include the child's content
|
|
116
|
+
if (parent_start === anchor || parent_start === end) {
|
|
117
|
+
// Save original so we can restore it when switching to non-RippleElement
|
|
118
|
+
if (modified_parent_branch === null) {
|
|
119
|
+
modified_parent_branch = parent_branch;
|
|
120
|
+
original_parent_start = parent_start;
|
|
121
|
+
}
|
|
122
|
+
parent_branch.s.start = child_start;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
71
126
|
value = next_value;
|
|
72
127
|
is_element = true;
|
|
73
128
|
initialized = true;
|
|
@@ -89,6 +144,13 @@ export function expression(node, get_value) {
|
|
|
89
144
|
if (child_block !== null) {
|
|
90
145
|
destroy_block(child_block);
|
|
91
146
|
child_block = null;
|
|
147
|
+
// Restore parent branch's start to original value since the child's DOM nodes
|
|
148
|
+
// have been removed and the old start reference would be stale
|
|
149
|
+
if (modified_parent_branch !== null && modified_parent_branch.s !== null) {
|
|
150
|
+
modified_parent_branch.s.start = original_parent_start;
|
|
151
|
+
modified_parent_branch = null;
|
|
152
|
+
original_parent_start = null;
|
|
153
|
+
}
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
if (is_hydration_marker) {
|
package/src/utils/builders.js
CHANGED
|
@@ -513,6 +513,36 @@ export function ts_intersection_type(types, loc_info) {
|
|
|
513
513
|
return set_location(node, loc_info);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
/**
|
|
517
|
+
* @param {'string' | 'number' | 'boolean' | 'any' | 'void' | 'null' | 'undefined' | 'never' | 'unknown' | 'bigint' | 'symbol' | 'object'} keyword
|
|
518
|
+
* @param {AST.NodeWithLocation} [loc_info]
|
|
519
|
+
* @returns {AST.TypeNode}
|
|
520
|
+
*/
|
|
521
|
+
export function ts_keyword_type(keyword, loc_info) {
|
|
522
|
+
/** @type {Record<string, string>} */
|
|
523
|
+
const keyword_to_type = {
|
|
524
|
+
string: 'TSStringKeyword',
|
|
525
|
+
number: 'TSNumberKeyword',
|
|
526
|
+
boolean: 'TSBooleanKeyword',
|
|
527
|
+
any: 'TSAnyKeyword',
|
|
528
|
+
void: 'TSVoidKeyword',
|
|
529
|
+
null: 'TSNullKeyword',
|
|
530
|
+
undefined: 'TSUndefinedKeyword',
|
|
531
|
+
never: 'TSNeverKeyword',
|
|
532
|
+
unknown: 'TSUnknownKeyword',
|
|
533
|
+
bigint: 'TSBigIntKeyword',
|
|
534
|
+
symbol: 'TSSymbolKeyword',
|
|
535
|
+
object: 'TSObjectKeyword',
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const node = /** @type {AST.TypeNode} */ ({
|
|
539
|
+
type: keyword_to_type[keyword],
|
|
540
|
+
metadata: { path: [] },
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return set_location(node, loc_info);
|
|
544
|
+
}
|
|
545
|
+
|
|
516
546
|
/**
|
|
517
547
|
* @param {AST.Node} type_annotation
|
|
518
548
|
* @param {AST.NodeWithLocation} [loc_info]
|
|
@@ -167,8 +167,10 @@ describe('basic client > rendering & text', () => {
|
|
|
167
167
|
it('basic operations', () => {
|
|
168
168
|
component App() {
|
|
169
169
|
let &[count] = track(0);
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
const a = count++;
|
|
171
|
+
const b = ++count;
|
|
172
|
+
<div>{a}</div>
|
|
173
|
+
<div>{b}</div>
|
|
172
174
|
<div>{5}</div>
|
|
173
175
|
<div>{count}</div>
|
|
174
176
|
}
|
|
@@ -192,3 +192,49 @@ describe('composite > render', () => {
|
|
|
192
192
|
expect(div.innerHTML).not.toContain('<undefined');
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
+
|
|
196
|
+
describe('scoped styles with children', () => {
|
|
197
|
+
it('generates correct CSS hashes for wrapper and child with empty style in App', () => {
|
|
198
|
+
component Wrapper(&{ children }: { children?: Component }) {
|
|
199
|
+
<div class="green">
|
|
200
|
+
{'Wrapper'}
|
|
201
|
+
{children}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<style>
|
|
205
|
+
.green {
|
|
206
|
+
color: green;
|
|
207
|
+
}
|
|
208
|
+
</style>
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
component Child() {
|
|
212
|
+
<div class="red">{'Child'}</div>
|
|
213
|
+
|
|
214
|
+
<style>
|
|
215
|
+
.red {
|
|
216
|
+
color: red;
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
component App() {
|
|
222
|
+
<Wrapper>
|
|
223
|
+
<Child />
|
|
224
|
+
</Wrapper>
|
|
225
|
+
<style></style>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
render(App);
|
|
229
|
+
|
|
230
|
+
const wrapper = container.querySelector('.green');
|
|
231
|
+
const child = container.querySelector('.red');
|
|
232
|
+
|
|
233
|
+
const wrapper_classes = Array.from(wrapper.classList).filter((c) => c.startsWith('ripple-'));
|
|
234
|
+
const child_classes = Array.from(child.classList).filter((c) => c.startsWith('ripple-'));
|
|
235
|
+
|
|
236
|
+
expect(wrapper_classes).toHaveLength(1);
|
|
237
|
+
expect(child_classes).toHaveLength(1);
|
|
238
|
+
expect(wrapper_classes[0]).not.toBe(child_classes[0]);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -2498,3 +2498,104 @@ describe('early return in client components', () => {
|
|
|
2498
2498
|
});
|
|
2499
2499
|
});
|
|
2500
2500
|
});
|
|
2501
|
+
|
|
2502
|
+
describe('throw statements in if blocks', () => {
|
|
2503
|
+
it('allows if statement with throw in then body', () => {
|
|
2504
|
+
const code = `
|
|
2505
|
+
export default component App() {
|
|
2506
|
+
let error = true;
|
|
2507
|
+
if (error) {
|
|
2508
|
+
throw new Error('Test error');
|
|
2509
|
+
}
|
|
2510
|
+
<div>{'no error'}</div>
|
|
2511
|
+
}
|
|
2512
|
+
`;
|
|
2513
|
+
expect(() => {
|
|
2514
|
+
compile(code, 'test.ripple');
|
|
2515
|
+
}).not.toThrow();
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
it('allows if statement with throw in else body', () => {
|
|
2519
|
+
const code = `
|
|
2520
|
+
export default component App() {
|
|
2521
|
+
let error = false;
|
|
2522
|
+
if (error) {
|
|
2523
|
+
<div>{'no error'}</div>
|
|
2524
|
+
} else {
|
|
2525
|
+
throw new Error('Test error');
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
`;
|
|
2529
|
+
expect(() => {
|
|
2530
|
+
compile(code, 'test.ripple');
|
|
2531
|
+
}).not.toThrow();
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
it('allows if statement with throw in both bodies', () => {
|
|
2535
|
+
const code = `
|
|
2536
|
+
export default component App() {
|
|
2537
|
+
let mode = 'error';
|
|
2538
|
+
if (mode === 'a') {
|
|
2539
|
+
<div>{'a'}</div>
|
|
2540
|
+
} else if (mode === 'b') {
|
|
2541
|
+
<div>{'b'}</div>
|
|
2542
|
+
} else {
|
|
2543
|
+
throw new Error('Unknown mode');
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
`;
|
|
2547
|
+
expect(() => {
|
|
2548
|
+
compile(code, 'test.ripple');
|
|
2549
|
+
}).not.toThrow();
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
it('allows nested if with throw', () => {
|
|
2553
|
+
const code = `
|
|
2554
|
+
export default component App() {
|
|
2555
|
+
let a = true;
|
|
2556
|
+
let b = true;
|
|
2557
|
+
if (a) {
|
|
2558
|
+
if (b) {
|
|
2559
|
+
throw new Error('Both true');
|
|
2560
|
+
}
|
|
2561
|
+
<div>{'a only'}</div>
|
|
2562
|
+
}
|
|
2563
|
+
<div>{'rest'}</div>
|
|
2564
|
+
}
|
|
2565
|
+
`;
|
|
2566
|
+
expect(() => {
|
|
2567
|
+
compile(code, 'test.ripple');
|
|
2568
|
+
}).not.toThrow();
|
|
2569
|
+
});
|
|
2570
|
+
|
|
2571
|
+
it('allows throw with reactive condition', () => {
|
|
2572
|
+
const code = `
|
|
2573
|
+
export default component App() {
|
|
2574
|
+
let &[error] = track(false);
|
|
2575
|
+
if (error) {
|
|
2576
|
+
throw new Error('Error occurred');
|
|
2577
|
+
}
|
|
2578
|
+
<div>{'success'}</div>
|
|
2579
|
+
}
|
|
2580
|
+
`;
|
|
2581
|
+
expect(() => {
|
|
2582
|
+
compile(code, 'test.ripple');
|
|
2583
|
+
}).not.toThrow();
|
|
2584
|
+
});
|
|
2585
|
+
|
|
2586
|
+
it('allows throw with template in then body and throw in else body', () => {
|
|
2587
|
+
const code = `
|
|
2588
|
+
export default component App() {
|
|
2589
|
+
let error = true;
|
|
2590
|
+
if (error) {
|
|
2591
|
+
<div>{'error case'}</div>
|
|
2592
|
+
} else {
|
|
2593
|
+
throw new Error('No error');
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
`;
|
|
2597
|
+
expect(() => {
|
|
2598
|
+
compile(code, 'test.ripple');
|
|
2599
|
+
}).not.toThrow();
|
|
2600
|
+
});
|
|
2601
|
+
});
|