ripple 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +1 -1
  4. package/src/compiler/index.d.ts +3 -1
  5. package/src/compiler/phases/1-parse/index.js +170 -8
  6. package/src/compiler/phases/2-analyze/index.js +231 -20
  7. package/src/compiler/phases/3-transform/client/index.js +169 -77
  8. package/src/compiler/phases/3-transform/server/index.js +46 -3
  9. package/src/compiler/types/index.d.ts +19 -2
  10. package/src/compiler/types/parse.d.ts +1 -1
  11. package/src/compiler/utils.js +174 -0
  12. package/src/runtime/index-client.js +14 -4
  13. package/src/runtime/internal/client/composite.js +2 -2
  14. package/src/runtime/internal/client/expression.js +64 -2
  15. package/src/runtime/internal/client/portal.js +1 -1
  16. package/src/utils/builders.js +30 -0
  17. package/tests/client/basic/__snapshots__/basic.rendering.test.ripple.snap +1 -0
  18. package/tests/client/basic/basic.rendering.test.ripple +4 -2
  19. package/tests/client/composite/composite.render.test.ripple +46 -0
  20. package/tests/client/return.test.ripple +101 -0
  21. package/tests/client/tsx.test.ripple +486 -0
  22. package/tests/hydration/compiled/client/basic.js +8 -24
  23. package/tests/hydration/compiled/client/composite.js +6 -24
  24. package/tests/hydration/compiled/client/events.js +9 -54
  25. package/tests/hydration/compiled/client/for.js +59 -152
  26. package/tests/hydration/compiled/client/head.js +5 -20
  27. package/tests/hydration/compiled/client/hmr.js +2 -8
  28. package/tests/hydration/compiled/client/html.js +59 -226
  29. package/tests/hydration/compiled/client/if-children.js +6 -22
  30. package/tests/hydration/compiled/client/mixed-control-flow.js +18 -66
  31. package/tests/hydration/compiled/client/nested-control-flow.js +92 -368
  32. package/tests/hydration/compiled/client/portal.js +4 -16
  33. package/tests/hydration/compiled/client/reactivity.js +7 -40
  34. package/tests/hydration/compiled/client/return.js +1 -4
  35. package/tests/hydration/compiled/client/try.js +2 -2
  36. package/tests/utils/compiler-compat-config.test.js +38 -0
  37. package/tests/utils/vite-plugin-config.test.js +113 -0
  38. package/tsconfig.json +2 -0
  39. package/tsconfig.typecheck.json +2 -1
  40. package/types/index.d.ts +2 -12
@@ -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, compat?: CompatOptions }} options
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
- }, options.compat);
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, compat?: CompatOptions }} options
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
- }, options.compat);
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, {}, block));
77
+ with_ns(ns, () => props.children.render(child_anchor, block));
78
78
  } else {
79
- props.children.render(child_anchor, {}, block);
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, {}, block);
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) {
@@ -72,7 +72,7 @@ export function Portal(_, props) {
72
72
 
73
73
  b = branch(() => {
74
74
  if (is_ripple_element(children)) {
75
- children.render(/** @type {Text} */ (anchor), {}, block);
75
+ children.render(/** @type {Text} */ (anchor), block);
76
76
  }
77
77
  });
78
78
 
@@ -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]
@@ -52,6 +52,7 @@ exports[`basic client > rendering & text > should handle lexical scopes correctl
52
52
  <div>
53
53
  <section>
54
54
  Nested scope variable
55
+ <!---->
55
56
  </section>
56
57
 
57
58
  </div>
@@ -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
- <div>{count++}</div>
171
- <div>{++count}</div>
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
+ });