ripple 0.2.89 → 0.2.91

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/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.2.89",
6
+ "version": "0.2.91",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -455,10 +455,31 @@ function RipplePlugin(config) {
455
455
  jsx_parseExpressionContainer() {
456
456
  let node = this.startNode();
457
457
  this.next();
458
+ let tracked = false;
459
+
460
+ if (this.value === 'html') {
461
+ node.html = true;
462
+ this.next();
463
+ if (this.type === tt.braceR) {
464
+ this.raise(
465
+ this.start,
466
+ '"html" is a Ripple keyword and must be used in the form {html some_content}',
467
+ );
468
+ }
469
+ if (this.type.label === '@') {
470
+ this.next(); // consume @
471
+ tracked = true;
472
+ }
473
+ }
458
474
 
459
475
  node.expression =
460
476
  this.type === tt.braceR ? this.jsx_parseEmptyExpression() : this.parseExpression();
461
477
  this.expect(tt.braceR);
478
+
479
+ if (tracked && node.expression.type === 'Identifier') {
480
+ node.expression.tracked = true;
481
+ }
482
+
462
483
  return this.finishNode(node, 'JSXExpressionContainer');
463
484
  }
464
485
 
@@ -954,7 +975,8 @@ function RipplePlugin(config) {
954
975
 
955
976
  if (this.type.label === '{') {
956
977
  const node = this.jsx_parseExpressionContainer();
957
- node.type = 'Text';
978
+ node.type = node.html ? 'Html' : 'Text';
979
+ delete node.html;
958
980
  body.push(node);
959
981
  } else if (this.type.label === '}') {
960
982
  return;
@@ -46,8 +46,7 @@ function add_ripple_internal_import(context) {
46
46
 
47
47
  function visit_function(node, context) {
48
48
  if (context.state.to_ts) {
49
- context.next(context.state);
50
- return;
49
+ return context.next(context.state);
51
50
  }
52
51
  const metadata = node.metadata;
53
52
  const state = context.state;
@@ -133,9 +132,7 @@ function visit_title_element(node, context) {
133
132
  ),
134
133
  );
135
134
  } else {
136
- context.state.init.push(
137
- b.stmt(b.assignment('=', b.id('_$_.document.title'), result)),
138
- );
135
+ context.state.init.push(b.stmt(b.assignment('=', b.id('_$_.document.title'), result)));
139
136
  }
140
137
  }
141
138
 
@@ -346,7 +343,7 @@ const visitors = {
346
343
 
347
344
  return b.new(
348
345
  b.id('TrackedObject'),
349
- b.object(node.properties.map((prop) => context.visit(prop)))
346
+ b.object(node.properties.map((prop) => context.visit(prop))),
350
347
  );
351
348
  }
352
349
 
@@ -723,9 +720,10 @@ const visitors = {
723
720
  const metadata = { tracking: false, await: false };
724
721
  let expression = visit(class_attribute.value, { ...state, metadata });
725
722
 
726
- const hash_arg = node.metadata.scoped && state.component.css
727
- ? b.literal(state.component.css.hash)
728
- : undefined;
723
+ const hash_arg =
724
+ node.metadata.scoped && state.component.css
725
+ ? b.literal(state.component.css.hash)
726
+ : undefined;
729
727
  const is_html = context.state.metadata.namespace === 'html' && node.id.name !== 'svg';
730
728
 
731
729
  if (metadata.tracking) {
@@ -897,7 +895,11 @@ const visitors = {
897
895
  }),
898
896
  ];
899
897
 
900
- return b.function(node.id, node.params, b.block(body_statements));
898
+ return b.function(
899
+ node.id,
900
+ node.params.map((param) => context.visit(param, { ...context.state, metadata })),
901
+ b.block(body_statements),
902
+ );
901
903
  }
902
904
 
903
905
  let props = b.id('__props');
@@ -1001,8 +1003,7 @@ const visitors = {
1001
1003
 
1002
1004
  UpdateExpression(node, context) {
1003
1005
  if (context.state.to_ts) {
1004
- context.next();
1005
- return;
1006
+ return context.next();
1006
1007
  }
1007
1008
  const argument = node.argument;
1008
1009
 
@@ -1048,8 +1049,7 @@ const visitors = {
1048
1049
 
1049
1050
  ForOfStatement(node, context) {
1050
1051
  if (!is_inside_component(context)) {
1051
- context.next();
1052
- return;
1052
+ return context.next();
1053
1053
  }
1054
1054
  const is_controlled = node.is_controlled;
1055
1055
  const index = node.index;
@@ -1091,8 +1091,7 @@ const visitors = {
1091
1091
 
1092
1092
  IfStatement(node, context) {
1093
1093
  if (!is_inside_component(context)) {
1094
- context.next();
1095
- return;
1094
+ return context.next();
1096
1095
  }
1097
1096
  context.state.template.push('<!>');
1098
1097
 
@@ -1175,8 +1174,7 @@ const visitors = {
1175
1174
 
1176
1175
  TryStatement(node, context) {
1177
1176
  if (!is_inside_component(context)) {
1178
- context.next();
1179
- return;
1177
+ return context.next();
1180
1178
  }
1181
1179
  context.state.template.push('<!>');
1182
1180
 
@@ -1311,6 +1309,9 @@ function transform_ts_child(node, context) {
1311
1309
 
1312
1310
  if (node.type === 'Text') {
1313
1311
  state.init.push(b.stmt(visit(node.expression, { ...state })));
1312
+ } else if (node.type === 'Html') {
1313
+ // Do we need to do something special here?
1314
+ state.init.push(b.stmt(visit(node.expression, { ...state })));
1314
1315
  } else if (node.type === 'Element') {
1315
1316
  const type = node.id.name;
1316
1317
  const children = [];
@@ -1330,12 +1331,25 @@ function transform_ts_child(node, context) {
1330
1331
  if (attr.type === 'Attribute') {
1331
1332
  const metadata = { await: false };
1332
1333
  const name = visit(attr.name, { ...state, metadata });
1333
- const value = visit(attr.value, { ...state, metadata });
1334
- const jsx_name = b.jsx_id(name.name);
1335
- if (name.name === 'children') {
1334
+ const value =
1335
+ attr.value === null ? b.literal(true) : visit(attr.value, { ...state, metadata });
1336
+
1337
+ // Handle both regular identifiers and tracked identifiers
1338
+ let prop_name;
1339
+ if (name.type === 'Identifier') {
1340
+ prop_name = name.name;
1341
+ } else if (name.type === 'MemberExpression' && name.object.type === 'Identifier') {
1342
+ // For tracked attributes like {@count}, use the original name
1343
+ prop_name = name.object.name;
1344
+ } else {
1345
+ prop_name = attr.name.name || 'unknown';
1346
+ }
1347
+
1348
+ const jsx_name = b.jsx_id(prop_name);
1349
+ if (prop_name === 'children') {
1336
1350
  has_children_props = true;
1337
1351
  }
1338
- jsx_name.loc = name.loc;
1352
+ jsx_name.loc = attr.name.loc || name.loc;
1339
1353
 
1340
1354
  return b.jsx_attribute(jsx_name, b.jsx_expression_container(value));
1341
1355
  } else if (attr.type === 'SpreadAttribute') {
@@ -1469,7 +1483,7 @@ function transform_ts_child(node, context) {
1469
1483
 
1470
1484
  state.init.push(b.try(try_body, catch_handler, finally_block));
1471
1485
  } else if (node.type === 'Component') {
1472
- const component = visit(node, context.state);
1486
+ const component = visit(node, state);
1473
1487
 
1474
1488
  state.init.push(component);
1475
1489
  } else {
@@ -1491,6 +1505,7 @@ function transform_children(children, context) {
1491
1505
  node.type === 'IfStatement' ||
1492
1506
  node.type === 'TryStatement' ||
1493
1507
  node.type === 'ForOfStatement' ||
1508
+ node.type === 'Html' ||
1494
1509
  (node.type === 'Element' &&
1495
1510
  (node.id.type !== 'Identifier' || !is_element_dom_element(node))),
1496
1511
  ) ||
@@ -1585,6 +1600,14 @@ function transform_children(children, context) {
1585
1600
  visit(node, { ...state, flush_node, namespace: state.namespace });
1586
1601
  } else if (node.type === 'HeadElement') {
1587
1602
  visit(node, { ...state, flush_node, namespace: state.namespace });
1603
+ } else if (node.type === 'Html') {
1604
+ const metadata = { tracking: false, await: false };
1605
+ const expression = visit(node.expression, { ...state, metadata });
1606
+
1607
+ context.state.template.push('<!>');
1608
+
1609
+ const id = flush_node();
1610
+ state.update.push(b.stmt(b.call('_$_.html', id, b.thunk(expression))));
1588
1611
  } else if (node.type === 'Text') {
1589
1612
  const metadata = { tracking: false, await: false };
1590
1613
  const expression = visit(node.expression, { ...state, metadata });
@@ -1633,7 +1656,7 @@ function transform_children(children, context) {
1633
1656
  visit_head_element(head_element, context);
1634
1657
  }
1635
1658
 
1636
- if (context.state.inside_head) {
1659
+ if (context.state.inside_head) {
1637
1660
  const title_element = children.find(
1638
1661
  (node) =>
1639
1662
  node.type === 'Element' && node.id.type === 'Identifier' && node.id.name === 'title',
@@ -1676,7 +1699,12 @@ function transform_body(body, { visit, state }) {
1676
1699
  transform_children(body, { visit, state: body_state, root: true });
1677
1700
 
1678
1701
  if (body_state.update.length > 0) {
1679
- body_state.init.push(b.stmt(b.call('_$_.render', b.thunk(b.block(body_state.update)))));
1702
+ if (state.to_ts) {
1703
+ // In TypeScript mode, just add the update statements directly
1704
+ body_state.init.push(...body_state.update);
1705
+ } else {
1706
+ body_state.init.push(b.stmt(b.call('_$_.render', b.thunk(b.block(body_state.update)))));
1707
+ }
1680
1708
  }
1681
1709
 
1682
1710
  return [...body_state.setup, ...body_state.init, ...body_state.final];
@@ -7,14 +7,87 @@ export const mapping_data = {
7
7
  navigation: true,
8
8
  };
9
9
 
10
+ /**
11
+ * Helper to find a meaningful token boundary by looking for word boundaries,
12
+ * punctuation, or whitespace
13
+ * @param {string} text
14
+ * @param {number} start
15
+ * @param {number} direction
16
+ */
17
+ function findTokenBoundary(text, start, direction = 1) {
18
+ if (start < 0 || start >= text.length) return start;
19
+
20
+ let pos = start;
21
+ /** @param {string} c */
22
+ const isAlphaNum = (c) => /[a-zA-Z0-9_$]/.test(c);
23
+
24
+ // If we're at whitespace or punctuation, find the next meaningful character
25
+ while (pos >= 0 && pos < text.length && /\s/.test(text[pos])) {
26
+ pos += direction;
27
+ }
28
+
29
+ if (pos < 0 || pos >= text.length) return start;
30
+
31
+ // If we're in the middle of a word/identifier, find the boundary
32
+ if (isAlphaNum(text[pos])) {
33
+ if (direction > 0) {
34
+ while (pos < text.length && isAlphaNum(text[pos])) pos++;
35
+ } else {
36
+ while (pos >= 0 && isAlphaNum(text[pos])) pos--;
37
+ pos++; // Adjust back to start of token
38
+ }
39
+ } else {
40
+ // For punctuation, just move one character in the given direction
41
+ pos += direction;
42
+ }
43
+
44
+ return Math.max(0, Math.min(text.length, pos));
45
+ }
46
+
47
+ /**
48
+ * Check if source and generated content are meaningfully similar
49
+ * @param {string} sourceContent
50
+ * @param {string} generatedContent
51
+ */
52
+ function isValidMapping(sourceContent, generatedContent) {
53
+ // Remove whitespace for comparison
54
+ const cleanSource = sourceContent.replace(/\s+/g, '');
55
+ const cleanGenerated = generatedContent.replace(/\s+/g, '');
56
+
57
+ // If either is empty, skip
58
+ if (!cleanSource || !cleanGenerated) return false;
59
+
60
+ // Skip obvious template transformations that don't make sense to map
61
+ const templateTransforms = [
62
+ /^\{.*\}$/, // Curly brace expressions
63
+ /^<.*>$/, // HTML tags
64
+ /^\(\(\)\s*=>\s*\{$/, // Generated function wrappers
65
+ /^\}\)\(\)\}$/, // Generated function closures
66
+ ];
67
+
68
+ for (const transform of templateTransforms) {
69
+ if (transform.test(cleanSource) || transform.test(cleanGenerated)) {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // Check if content is similar (exact match, or generated contains source)
75
+ if (cleanSource === cleanGenerated) return true;
76
+ if (cleanGenerated.includes(cleanSource)) return true;
77
+ if (cleanSource.includes(cleanGenerated) && cleanGenerated.length > 2) return true;
78
+
79
+ return false;
80
+ }
81
+
10
82
  /**
11
83
  * Convert esrap SourceMap to Volar mappings
12
- * @param {object} source_map
84
+ * @param {{ mappings: string }} source_map
13
85
  * @param {string} source
14
86
  * @param {string} generated_code
15
87
  * @returns {object}
16
88
  */
17
89
  export function convert_source_map_to_mappings(source_map, source, generated_code) {
90
+ /** @type {Array<{sourceOffsets: number[], generatedOffsets: number[], lengths: number[], data: any}>} */
18
91
  const mappings = [];
19
92
 
20
93
  // Decode the VLQ mappings from esrap
@@ -22,6 +95,7 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
22
95
 
23
96
  let generated_offset = 0;
24
97
  const generated_lines = generated_code.split('\n');
98
+ const source_lines = source.split('\n');
25
99
 
26
100
  // Process each line of generated code
27
101
  for (let generated_line = 0; generated_line < generated_lines.length; generated_line++) {
@@ -38,7 +112,6 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
38
112
  }
39
113
 
40
114
  // Calculate source offset
41
- const source_lines = source.split('\n');
42
115
  let source_offset = 0;
43
116
  for (let i = 0; i < Math.min(source_line, source_lines.length - 1); i++) {
44
117
  source_offset += source_lines[i].length + 1; // +1 for newline
@@ -48,41 +121,125 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
48
121
  // Calculate generated offset
49
122
  const current_generated_offset = generated_offset + generated_column;
50
123
 
51
- // Determine segment length (look ahead to next mapping or end of line)
52
- const next_mapping = line_mappings[line_mappings.indexOf(mapping) + 1];
53
- let segment_length = next_mapping
54
- ? next_mapping[0] - generated_column
55
- : Math.max(1, line.length - generated_column);
56
-
57
- // Determine the actual segment content
58
- const generated_content = generated_code.substring(
59
- current_generated_offset,
60
- current_generated_offset + segment_length,
61
- );
62
- const source_content = source.substring(source_offset, source_offset + segment_length);
63
-
64
- // Skip mappings for RefAttribute syntax to avoid overlapping sourcemaps
65
- if (source_content.includes('{ref ') || source_content.match(/\{\s*ref\s+/)) {
66
- continue;
124
+ // Find meaningful token boundaries for source content
125
+ const source_token_end = findTokenBoundary(source, source_offset, 1);
126
+ const source_token_start = findTokenBoundary(source, source_offset, -1);
127
+
128
+ // Find meaningful token boundaries for generated content
129
+ const generated_token_end = findTokenBoundary(generated_code, current_generated_offset, 1);
130
+ const generated_token_start = findTokenBoundary(generated_code, current_generated_offset, -1);
131
+
132
+ // Extract potential source content (prefer forward boundary but try both directions)
133
+ let best_source_content = source.substring(source_offset, source_token_end);
134
+ let best_generated_content = generated_code.substring(current_generated_offset, generated_token_end);
135
+
136
+ // Try different segment boundaries to find the best match
137
+ const candidates = [
138
+ // Forward boundaries
139
+ {
140
+ source: source.substring(source_offset, source_token_end),
141
+ generated: generated_code.substring(current_generated_offset, generated_token_end)
142
+ },
143
+ // Backward boundaries
144
+ {
145
+ source: source.substring(source_token_start, source_offset + 1),
146
+ generated: generated_code.substring(generated_token_start, current_generated_offset + 1)
147
+ },
148
+ // Single character
149
+ {
150
+ source: source.charAt(source_offset),
151
+ generated: generated_code.charAt(current_generated_offset)
152
+ },
153
+ // Try to find exact matches in nearby content
154
+ ];
155
+
156
+ // Look for the best candidate match
157
+ let best_match = null;
158
+ for (const candidate of candidates) {
159
+ if (isValidMapping(candidate.source, candidate.generated)) {
160
+ best_match = candidate;
161
+ break;
162
+ }
67
163
  }
68
-
69
- // Fix for children mapping: when generated content is "children",
70
- // it should only map to the component name in the source, not include attributes
71
- if (generated_content === 'children') {
72
- // Look for the component name in the source content
73
- const component_name_match = source_content.match(/^(\w+)/);
74
- if (component_name_match) {
75
- const component_name = component_name_match[1];
76
- segment_length = component_name.length;
164
+
165
+ // If no good match found, try extracting identifiers/keywords
166
+ if (!best_match) {
167
+ const sourceIdMatch = source.substring(source_offset).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
168
+ const generatedIdMatch = generated_code.substring(current_generated_offset).match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
169
+
170
+ if (sourceIdMatch && generatedIdMatch && sourceIdMatch[0] === generatedIdMatch[0]) {
171
+ best_match = {
172
+ source: sourceIdMatch[0],
173
+ generated: generatedIdMatch[0]
174
+ };
77
175
  }
78
176
  }
79
-
80
- mappings.push({
81
- sourceOffsets: [source_offset],
82
- generatedOffsets: [current_generated_offset],
83
- lengths: [segment_length],
84
- data: mapping_data,
85
- });
177
+
178
+ // Handle special cases for Ripple keywords that might not have generated equivalents
179
+ if (!best_match || best_match.source.length === 0) {
180
+ continue;
181
+ }
182
+
183
+ // Special handling for Ripple-specific syntax that may be omitted in generated code
184
+ const sourceAtOffset = source.substring(source_offset, source_offset + 10);
185
+ if (sourceAtOffset.includes('index ')) {
186
+ // For the 'index' keyword, create a mapping even if there's no generated equivalent
187
+ const indexMatch = sourceAtOffset.match(/index\s+/);
188
+ if (indexMatch) {
189
+ best_match = {
190
+ source: indexMatch[0].trim(),
191
+ generated: '' // Empty generated content for keywords that are transformed away
192
+ };
193
+ }
194
+ }
195
+
196
+ // Skip if we still don't have a valid source match
197
+ if (!best_match || best_match.source.length === 0) {
198
+ continue;
199
+ }
200
+
201
+ // Skip mappings for complex RefAttribute syntax to avoid overlapping sourcemaps,
202
+ // but allow simple 'ref' keyword mappings for IntelliSense
203
+ if (best_match.source.includes('{ref ') && best_match.source.length > 10) {
204
+ // Skip complex ref expressions like '{ref (node) => { ... }}'
205
+ continue;
206
+ }
207
+
208
+ // Allow simple 'ref' keyword mappings for IntelliSense
209
+ if (best_match.source.trim() === 'ref' && best_match.generated.length === 0) {
210
+ // This is just the ref keyword, allow it for syntax support
211
+ // but map it to current position since there's no generated equivalent
212
+ }
213
+
214
+ // Calculate actual offsets and lengths for the best match
215
+ let actual_source_offset, actual_generated_offset;
216
+
217
+ if (best_match.generated.length > 0) {
218
+ actual_source_offset = source.indexOf(best_match.source, source_offset - best_match.source.length);
219
+ actual_generated_offset = generated_code.indexOf(best_match.generated, current_generated_offset - best_match.generated.length);
220
+ } else {
221
+ // For keywords with no generated equivalent, use the exact source position
222
+ actual_source_offset = source_offset;
223
+ actual_generated_offset = current_generated_offset; // Map to current position in generated code
224
+ }
225
+
226
+ // Use the match we found, but fall back to original positions if indexOf fails
227
+ const final_source_offset = actual_source_offset !== -1 ? actual_source_offset : source_offset;
228
+ const final_generated_offset = actual_generated_offset !== -1 ? actual_generated_offset : current_generated_offset; // Avoid duplicate mappings by checking if we already have this exact mapping
229
+ const isDuplicate = mappings.some(existing =>
230
+ existing.sourceOffsets[0] === final_source_offset &&
231
+ existing.generatedOffsets[0] === final_generated_offset &&
232
+ existing.lengths[0] === best_match.source.length
233
+ );
234
+
235
+ if (!isDuplicate) {
236
+ mappings.push({
237
+ sourceOffsets: [final_source_offset],
238
+ generatedOffsets: [final_generated_offset],
239
+ lengths: [best_match.source.length],
240
+ data: mapping_data,
241
+ });
242
+ }
86
243
  }
87
244
 
88
245
  // Add line length + 1 for newline (except for last line)
@@ -92,6 +249,9 @@ export function convert_source_map_to_mappings(source_map, source, generated_cod
92
249
  }
93
250
  }
94
251
 
252
+ // Sort mappings by source offset for better organization
253
+ mappings.sort((a, b) => a.sourceOffsets[0] - b.sourceOffsets[0]);
254
+
95
255
  return {
96
256
  code: generated_code,
97
257
  mappings,
@@ -0,0 +1,73 @@
1
+ /** @import { Block, Derived } from '#client' */
2
+ import { safe_scope, tracked, get, derived, set } from './internal/client/runtime.js';
3
+
4
+ var init = false;
5
+
6
+ export class TrackedDate extends Date {
7
+ #time;
8
+ /** @type {Map<keyof Date, Derived>} */
9
+ #deriveds = new Map();
10
+ /** @type {Block} */
11
+ #block;
12
+
13
+ /** @param {any[]} params */
14
+ constructor(...params) {
15
+ // @ts-ignore
16
+ super(...params);
17
+
18
+ var block = this.#block = safe_scope();
19
+ this.#time = tracked(super.getTime(), block);
20
+
21
+ if (!init) this.#init();
22
+ }
23
+
24
+ #init() {
25
+ init = true;
26
+
27
+ var proto = TrackedDate.prototype;
28
+ var date_proto = Date.prototype;
29
+
30
+ var methods = /** @type {Array<keyof Date & string>} */ (
31
+ Object.getOwnPropertyNames(date_proto)
32
+ );
33
+
34
+ for (const method of methods) {
35
+ if (method.startsWith('get') || method.startsWith('to') || method === 'valueOf') {
36
+ // @ts-ignore
37
+ proto[method] = function (...args) {
38
+ // don't memoize if there are arguments
39
+ // @ts-ignore
40
+ if (args.length > 0) {
41
+ get(this.#time);
42
+ // @ts-ignore
43
+ return date_proto[method].apply(this, args);
44
+ }
45
+
46
+ var d = this.#deriveds.get(method);
47
+
48
+ if (d === undefined) {
49
+ d = derived(() => {
50
+ get(this.#time);
51
+ // @ts-ignore
52
+ return date_proto[method].apply(this, args);
53
+ }, this.#block);
54
+
55
+ this.#deriveds.set(method, d);
56
+ }
57
+
58
+ return get(d);
59
+ };
60
+ }
61
+
62
+ if (method.startsWith('set')) {
63
+ // @ts-ignore
64
+ proto[method] = function (...args) {
65
+ // @ts-ignore
66
+ var result = date_proto[method].apply(this, args);
67
+ set(this.#time, date_proto.getTime.call(this), this.#block);
68
+ return result;
69
+ };
70
+ }
71
+ }
72
+ }
73
+ }
@@ -15,38 +15,38 @@ export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
15
15
  * @returns {() => void}
16
16
  */
17
17
  export function mount(component, options) {
18
- init_operations();
18
+ init_operations();
19
19
 
20
- const props = options.props || {};
21
- const target = options.target;
22
- const anchor = create_anchor();
20
+ const props = options.props || {};
21
+ const target = options.target;
22
+ const anchor = create_anchor();
23
23
 
24
- // Clear target content in case of SSR
25
- if (target.firstChild) {
26
- target.textContent = '';
27
- }
24
+ // Clear target content in case of SSR
25
+ if (target.firstChild) {
26
+ target.textContent = '';
27
+ }
28
28
 
29
- target.append(anchor);
29
+ target.append(anchor);
30
30
 
31
- const cleanup_events = handle_root_events(target);
31
+ const cleanup_events = handle_root_events(target);
32
32
 
33
- const _root = root(() => {
34
- component(anchor, props, active_block);
35
- });
33
+ const _root = root(() => {
34
+ component(anchor, props, active_block);
35
+ });
36
36
 
37
- return () => {
38
- cleanup_events();
39
- destroy_block(_root);
40
- };
37
+ return () => {
38
+ cleanup_events();
39
+ destroy_block(_root);
40
+ };
41
41
  }
42
42
 
43
43
  export { create_context as createContext } from './internal/client/context.js';
44
44
 
45
45
  export {
46
- flush_sync as flushSync,
47
- track,
48
- track_split as trackSplit,
49
- untrack,
46
+ flush_sync as flushSync,
47
+ track,
48
+ track_split as trackSplit,
49
+ untrack,
50
50
  } from './internal/client/runtime.js';
51
51
 
52
52
  export { TrackedArray } from './array.js';
@@ -57,6 +57,8 @@ export { TrackedSet } from './set.js';
57
57
 
58
58
  export { TrackedMap } from './map.js';
59
59
 
60
+ export { TrackedDate } from './date.js';
61
+
60
62
  export { keyed } from './internal/client/for.js';
61
63
 
62
64
  export { user_effect as effect } from './internal/client/blocks.js';
@@ -318,6 +318,20 @@ export function is_destroyed(target_block) {
318
318
  return true;
319
319
  }
320
320
 
321
+ /**
322
+ * @param {Node | null} node
323
+ * @param {Node} end
324
+ */
325
+ export function remove_block_dom(node, end) {
326
+ while (node !== null) {
327
+ /** @type {Node | null} */
328
+ var next = node === end ? null : next_sibling(node);
329
+
330
+ /** @type {Element | Text | Comment} */ (node).remove();
331
+ node = next;
332
+ }
333
+ }
334
+
321
335
  /**
322
336
  * @param {Block} block
323
337
  * @param {boolean} [remove_dom]
@@ -330,16 +344,7 @@ export function destroy_block(block, remove_dom = true) {
330
344
 
331
345
  if ((remove_dom && (f & (BRANCH_BLOCK | ROOT_BLOCK)) !== 0) || (f & HEAD_BLOCK) !== 0) {
332
346
  var s = block.s;
333
- var node = s.start;
334
- var end = s.end;
335
-
336
- while (node !== null) {
337
- var next = node === end ? null : next_sibling(node);
338
-
339
- node.remove();
340
- node = next;
341
- }
342
-
347
+ remove_block_dom(s.start, s.end);
343
348
  removed = true;
344
349
  }
345
350
 
@@ -0,0 +1,41 @@
1
+ /** @import { Block } from '#client' */
2
+
3
+ import { remove_block_dom, render } from './blocks.js';
4
+ import { first_child } from './operations.js';
5
+ import { active_block } from './runtime.js';
6
+ import { assign_nodes, create_fragment_from_html } from './template.js';
7
+
8
+ /**
9
+ * Renders dynamic HTML content into the DOM by inserting it before the anchor node.
10
+ * Manages the lifecycle of HTML blocks, removing old content and inserting new content.
11
+ *
12
+ * TODO handle SVG/MathML
13
+ *
14
+ * @param {ChildNode} node
15
+ * @param {() => string} get_html
16
+ * @returns {void}
17
+ */
18
+ export function html(node, get_html) {
19
+ /** @type {ChildNode} */
20
+ var anchor = node;
21
+ /** @type {string} */
22
+ var html = '';
23
+
24
+ render(() => {
25
+ var block = /** @type {Block} */ (active_block);
26
+ html = get_html() + '';
27
+
28
+ if (block.s !== null && block.s.start !== null) {
29
+ remove_block_dom(block.s.start, /** @type {Node} */ (block.s.end));
30
+ block.s.start = block.s.end = null;
31
+ }
32
+
33
+ if (html === '') return;
34
+ /** @type {DocumentFragment} */
35
+ var node = create_fragment_from_html(html);
36
+
37
+ assign_nodes(/** @type {Node } */ (first_child(node)), /** @type {Node} */ (node.lastChild));
38
+
39
+ anchor.before(node);
40
+ });
41
+ }
@@ -64,4 +64,6 @@ export { tracked_object } from '../../object.js';
64
64
 
65
65
  export { head } from './head.js';
66
66
 
67
- export { script } from './script.js';
67
+ export { script } from './script.js';
68
+
69
+ export { html } from './html.js';
@@ -35,7 +35,7 @@ export function assign_nodes(start, end) {
35
35
  * @param {boolean} use_mathml_namespace - Whether to use MathML namespace.
36
36
  * @returns {DocumentFragment}
37
37
  */
38
- function create_fragment_from_html(html, use_svg_namespace = false, use_mathml_namespace = false) {
38
+ export function create_fragment_from_html(html, use_svg_namespace = false, use_mathml_namespace = false) {
39
39
  if (use_svg_namespace) {
40
40
  return from_namespace(html, 'svg');
41
41
  }
@@ -0,0 +1,40 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`html directive > renders dynamic html 1`] = `
4
+ <div>
5
+ <!---->
6
+ <div>
7
+ Test
8
+ </div>
9
+ <!---->
10
+ <button>
11
+ Update
12
+ </button>
13
+
14
+ </div>
15
+ `;
16
+
17
+ exports[`html directive > renders dynamic html 2`] = `
18
+ <div>
19
+ <!---->
20
+ <div>
21
+ Updated
22
+ </div>
23
+ <!---->
24
+ <button>
25
+ Update
26
+ </button>
27
+
28
+ </div>
29
+ `;
30
+
31
+ exports[`html directive > renders static html 1`] = `
32
+ <div>
33
+ <!---->
34
+ <div>
35
+ Test
36
+ </div>
37
+ <!---->
38
+
39
+ </div>
40
+ `;
@@ -0,0 +1,392 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, TrackedDate, track } from 'ripple';
3
+
4
+ describe('TrackedDate', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container,
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('handles getTime() with reactive updates', () => {
24
+ component DateTest() {
25
+ let date = new TrackedDate(2025, 0, 1);
26
+ let time = track(() => date.getTime());
27
+
28
+ <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
29
+ <pre>{@time}</pre>
30
+ }
31
+
32
+ render(DateTest);
33
+
34
+ const button = container.querySelector('button');
35
+ const initialTime = container.querySelector('pre').textContent;
36
+
37
+ button.click();
38
+ flushSync();
39
+
40
+ const newTime = container.querySelector('pre').textContent;
41
+ expect(newTime).not.toBe(initialTime);
42
+ expect(parseInt(newTime)).toBeGreaterThan(parseInt(initialTime));
43
+ });
44
+
45
+ it('handles getFullYear() with reactive updates', () => {
46
+ component DateTest() {
47
+ let date = new TrackedDate(2025, 5, 15);
48
+ let year = track(() => date.getFullYear());
49
+
50
+ <button onClick={() => date.setFullYear(2030)}>{'Change Year'}</button>
51
+ <pre>{@year}</pre>
52
+ }
53
+
54
+ render(DateTest);
55
+
56
+ const button = container.querySelector('button');
57
+
58
+ expect(container.querySelector('pre').textContent).toBe('2025');
59
+
60
+ button.click();
61
+ flushSync();
62
+
63
+ expect(container.querySelector('pre').textContent).toBe('2030');
64
+ });
65
+
66
+ it('handles getMonth() with reactive updates', () => {
67
+ component DateTest() {
68
+ let date = new TrackedDate(2025, 0, 15);
69
+ let month = track(() => date.getMonth());
70
+
71
+ <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
72
+ <pre>{@month}</pre>
73
+ }
74
+
75
+ render(DateTest);
76
+
77
+ const button = container.querySelector('button');
78
+
79
+ expect(container.querySelector('pre').textContent).toBe('0');
80
+
81
+ button.click();
82
+ flushSync();
83
+
84
+ expect(container.querySelector('pre').textContent).toBe('11');
85
+ });
86
+
87
+ it('handles getDate() with reactive updates', () => {
88
+ component DateTest() {
89
+ let date = new TrackedDate(2025, 0, 1);
90
+ let day = track(() => date.getDate());
91
+
92
+ <button onClick={() => date.setDate(15)}>{'Change Day'}</button>
93
+ <pre>{@day}</pre>
94
+ }
95
+
96
+ render(DateTest);
97
+
98
+ const button = container.querySelector('button');
99
+
100
+ expect(container.querySelector('pre').textContent).toBe('1');
101
+
102
+ button.click();
103
+ flushSync();
104
+
105
+ expect(container.querySelector('pre').textContent).toBe('15');
106
+ });
107
+
108
+ it('handles getDay() with reactive updates', () => {
109
+ component DateTest() {
110
+ let date = new TrackedDate(2025, 0, 1);
111
+ let dayOfWeek = track(() => date.getDay());
112
+
113
+ <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
114
+ <pre>{@dayOfWeek}</pre>
115
+ }
116
+
117
+ render(DateTest);
118
+
119
+ const button = container.querySelector('button');
120
+
121
+ expect(container.querySelector('pre').textContent).toBe('3');
122
+
123
+ button.click();
124
+ flushSync();
125
+
126
+ expect(container.querySelector('pre').textContent).toBe('4');
127
+ });
128
+
129
+ it('handles getHours() with reactive updates', () => {
130
+ component DateTest() {
131
+ let date = new TrackedDate(2025, 0, 1, 10, 30, 0);
132
+ let hours = track(() => date.getHours());
133
+
134
+ <button onClick={() => date.setHours(15)}>{'Change to 3 PM'}</button>
135
+ <pre>{@hours}</pre>
136
+ }
137
+
138
+ render(DateTest);
139
+
140
+ const button = container.querySelector('button');
141
+
142
+ expect(container.querySelector('pre').textContent).toBe('10');
143
+
144
+ button.click();
145
+ flushSync();
146
+
147
+ expect(container.querySelector('pre').textContent).toBe('15');
148
+ });
149
+
150
+ it('handles getMinutes() with reactive updates', () => {
151
+ component DateTest() {
152
+ let date = new TrackedDate(2025, 0, 1, 10, 15, 0);
153
+ let minutes = track(() => date.getMinutes());
154
+
155
+ <button onClick={() => date.setMinutes(45)}>{'Change Minutes'}</button>
156
+ <pre>{@minutes}</pre>
157
+ }
158
+
159
+ render(DateTest);
160
+
161
+ const button = container.querySelector('button');
162
+
163
+ expect(container.querySelector('pre').textContent).toBe('15');
164
+
165
+ button.click();
166
+ flushSync();
167
+
168
+ expect(container.querySelector('pre').textContent).toBe('45');
169
+ });
170
+
171
+ it('handles getSeconds() with reactive updates', () => {
172
+ component DateTest() {
173
+ let date = new TrackedDate(2025, 0, 1, 10, 15, 30);
174
+ let seconds = track(() => date.getSeconds());
175
+
176
+ <button onClick={() => date.setSeconds(45)}>{'Change Seconds'}</button>
177
+ <pre>{@seconds}</pre>
178
+ }
179
+
180
+ render(DateTest);
181
+
182
+ const button = container.querySelector('button');
183
+
184
+ expect(container.querySelector('pre').textContent).toBe('30');
185
+
186
+ button.click();
187
+ flushSync();
188
+
189
+ expect(container.querySelector('pre').textContent).toBe('45');
190
+ });
191
+
192
+ it('handles toISOString() with reactive updates', () => {
193
+ component DateTest() {
194
+ let date = new TrackedDate(2025, 0, 1, 12, 0, 0);
195
+ let isoString = track(() => date.toISOString());
196
+
197
+ <button onClick={() => date.setFullYear(2026)}>{'Change Year'}</button>
198
+ <pre>{@isoString}</pre>
199
+ }
200
+
201
+ render(DateTest);
202
+
203
+ const button = container.querySelector('button');
204
+ const initialISO = container.querySelector('pre').textContent;
205
+
206
+ expect(initialISO).toContain('2025');
207
+
208
+ button.click();
209
+ flushSync();
210
+
211
+ const newISO = container.querySelector('pre').textContent;
212
+
213
+ // Just verify that the ISO string changed after the year was updated
214
+ expect(newISO).not.toBe(initialISO);
215
+ expect(newISO.length).toBeGreaterThan(0);
216
+ });
217
+
218
+ it('handles toDateString() with reactive updates', () => {
219
+ component DateTest() {
220
+ let date = new TrackedDate(2025, 0, 1);
221
+ let dateString = track(() => date.toDateString());
222
+
223
+ <button onClick={() => date.setMonth(11)}>{'Change to December'}</button>
224
+ <pre>{@dateString}</pre>
225
+ }
226
+
227
+ render(DateTest);
228
+
229
+ const button = container.querySelector('button');
230
+ const initialDateString = container.querySelector('pre').textContent;
231
+
232
+ expect(initialDateString).toContain('Jan');
233
+
234
+ button.click();
235
+ flushSync();
236
+
237
+ const newDateString = container.querySelector('pre').textContent;
238
+ expect(newDateString).toContain('Dec');
239
+ expect(newDateString).not.toBe(initialDateString);
240
+ });
241
+
242
+ it('handles valueOf() with reactive updates', () => {
243
+ component DateTest() {
244
+ let date = new TrackedDate(2025, 0, 1);
245
+ let valueOf = track(() => date.valueOf());
246
+
247
+ <button onClick={() => date.setDate(2)}>{'Next Day'}</button>
248
+ <pre>{@valueOf}</pre>
249
+ }
250
+
251
+ render(DateTest);
252
+
253
+ const button = container.querySelector('button');
254
+ const initialValue = parseInt(container.querySelector('pre').textContent);
255
+
256
+ button.click();
257
+ flushSync();
258
+
259
+ const newValue = parseInt(container.querySelector('pre').textContent);
260
+ expect(newValue).toBeGreaterThan(initialValue);
261
+ expect(newValue - initialValue).toBe(24 * 60 * 60 * 1000);
262
+ });
263
+
264
+ it('handles multiple get methods reacting to same setTime change', () => {
265
+ component DateTest() {
266
+ let date = new TrackedDate(2025, 0, 1, 10, 30, 15);
267
+ let year = track(() => date.getFullYear());
268
+ let month = track(() => date.getMonth());
269
+ let day = track(() => date.getDate());
270
+ let hours = track(() => date.getHours());
271
+
272
+ <button onClick={() => date.setTime(new Date(2026, 5, 15, 14, 45, 30).getTime())}>{'Change All'}</button>
273
+ <div>
274
+ {'Year: '}
275
+ {@year}
276
+ </div>
277
+ <div>
278
+ {'Month: '}
279
+ {@month}
280
+ </div>
281
+ <div>
282
+ {'Day: '}
283
+ {@day}
284
+ </div>
285
+ <div>
286
+ {'Hours: '}
287
+ {@hours}
288
+ </div>
289
+ }
290
+
291
+ render(DateTest);
292
+
293
+ const button = container.querySelector('button');
294
+ const divs = container.querySelectorAll('div');
295
+
296
+ expect(divs[0].textContent).toBe('Year: 2025');
297
+ expect(divs[1].textContent).toBe('Month: 0');
298
+ expect(divs[2].textContent).toBe('Day: 1');
299
+ expect(divs[3].textContent).toBe('Hours: 10');
300
+
301
+ button.click();
302
+ flushSync();
303
+
304
+ expect(divs[0].textContent).toBe('Year: 2026');
305
+ expect(divs[1].textContent).toBe('Month: 5');
306
+ expect(divs[2].textContent).toBe('Day: 15');
307
+ expect(divs[3].textContent).toBe('Hours: 14');
308
+ });
309
+
310
+ it('handles constructor with different parameter combinations', () => {
311
+ component DateTest() {
312
+ let dateNow = new TrackedDate();
313
+ let dateFromString = new TrackedDate('2025-01-01');
314
+ let dateFromNumbers = new TrackedDate(2025, 0, 1);
315
+ let dateFromTimestamp = new TrackedDate(1735689600000);
316
+
317
+ let nowYear = track(() => dateNow.getFullYear());
318
+ let stringYear = track(() => dateFromString.getFullYear());
319
+ let numbersYear = track(() => dateFromNumbers.getFullYear());
320
+ let timestampYear = track(() => dateFromTimestamp.getFullYear());
321
+
322
+ <div>
323
+ {'Now: '}
324
+ {@nowYear}
325
+ </div>
326
+ <div>
327
+ {'String: '}
328
+ {@stringYear}
329
+ </div>
330
+ <div>
331
+ {'Numbers: '}
332
+ {@numbersYear}
333
+ </div>
334
+ <div>
335
+ {'Timestamp: '}
336
+ {@timestampYear}
337
+ </div>
338
+ }
339
+
340
+ render(DateTest);
341
+
342
+ const divs = container.querySelectorAll('div');
343
+ const currentYear = new Date().getFullYear();
344
+
345
+ expect(parseInt(divs[0].textContent.split(': ')[1])).toBe(currentYear);
346
+
347
+ // String date parsing may vary by timezone, just check it's a reasonable year
348
+ const stringYear = parseInt(divs[1].textContent.split(': ')[1]);
349
+ expect(stringYear).toBeGreaterThanOrEqual(2024);
350
+ expect(stringYear).toBeLessThanOrEqual(2025);
351
+ expect(divs[2].textContent).toBe('Numbers: 2025');
352
+
353
+ // Timestamp parsing may also vary by timezone
354
+ const timestampYear = parseInt(divs[3].textContent.split(': ')[1]);
355
+ expect(timestampYear).toBeGreaterThanOrEqual(2024);
356
+ expect(timestampYear).toBeLessThanOrEqual(2025);
357
+ });
358
+
359
+ it('handles get methods with arguments non-memoized', () => {
360
+ component DateTest() {
361
+ let date = new TrackedDate();
362
+ let localeDateString = track(() => date.toLocaleDateString('en-US'));
363
+ let localeTimeString = track(() => date.toLocaleTimeString('en-US'));
364
+
365
+ <button onClick={() => date.setFullYear(date.getFullYear() + 1)}>{'Next Year'}</button>
366
+ <div>
367
+ {'Date: '}
368
+ {@localeDateString}
369
+ </div>
370
+ <div>
371
+ {'Time: '}
372
+ {@localeTimeString}
373
+ </div>
374
+ }
375
+
376
+ render(DateTest);
377
+
378
+ const button = container.querySelector('button');
379
+ const divs = container.querySelectorAll('div');
380
+ const initialDate = divs[0].textContent;
381
+ const initialTime = divs[1].textContent;
382
+
383
+ button.click();
384
+ flushSync();
385
+
386
+ const newDate = divs[0].textContent;
387
+ const newTime = divs[1].textContent;
388
+
389
+ expect(newDate).not.toBe(initialDate);
390
+ expect(newTime).toBe(initialTime);
391
+ });
392
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, flushSync, track } from 'ripple';
3
+
4
+ describe('html directive', () => {
5
+ let container;
6
+
7
+ function render(component) {
8
+ mount(component, {
9
+ target: container
10
+ });
11
+ }
12
+
13
+ beforeEach(() => {
14
+ container = document.createElement('div');
15
+ document.body.appendChild(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ document.body.removeChild(container);
20
+ container = null;
21
+ });
22
+
23
+ it('renders static html', () => {
24
+ component App() {
25
+ let str = '<div>Test</div>';
26
+
27
+ {html str}
28
+ }
29
+
30
+ render(App);
31
+ expect(container).toMatchSnapshot();
32
+ });
33
+
34
+ it('renders dynamic html', () => {
35
+ component App() {
36
+ let str = track('<div>Test</div>');
37
+
38
+ {html @str}
39
+
40
+ <button onClick={() => { @str = '<div>Updated</div>'; }}>{'Update'}</button>
41
+ }
42
+
43
+ render(App);
44
+ expect(container).toMatchSnapshot();
45
+
46
+ const button = container.querySelector('button');
47
+ button.click();
48
+ flushSync();
49
+
50
+ expect(container).toMatchSnapshot();
51
+ });
52
+ });
package/types/index.d.ts CHANGED
@@ -12,13 +12,13 @@ export declare function flushSync<T>(fn: () => T): T;
12
12
  export declare function effect(fn: (() => void) | (() => () => void)): void;
13
13
 
14
14
  export interface TrackedArrayConstructor {
15
- new <T>(...elements: T[]): TrackedArray<T>; // must be used with `new`
16
- from<T>(arrayLike: ArrayLike<T>): TrackedArray<T>;
17
- of<T>(...items: T[]): TrackedArray<T>;
18
- fromAsync<T>(iterable: AsyncIterable<T>): Promise<TrackedArray<T>>;
15
+ new <T>(...elements: T[]): TrackedArray<T>; // must be used with `new`
16
+ from<T>(arrayLike: ArrayLike<T>): TrackedArray<T>;
17
+ of<T>(...items: T[]): TrackedArray<T>;
18
+ fromAsync<T>(iterable: AsyncIterable<T>): Promise<TrackedArray<T>>;
19
19
  }
20
20
 
21
- export interface TrackedArray<T> extends Array<T> {}
21
+ export interface TrackedArray<T> extends Array<T> { }
22
22
 
23
23
  export declare const TrackedArray: TrackedArrayConstructor;
24
24
 
@@ -38,10 +38,12 @@ export declare class TrackedSet<T> extends Set<T> {
38
38
  symmetricDifference(other: TrackedSet<T> | Set<T>): TrackedSet<T>;
39
39
  union(other: TrackedSet<T> | Set<T>): TrackedSet<T>;
40
40
  toJSON(): T[];
41
+ #private;
41
42
  }
42
43
 
43
44
  export declare class TrackedMap<K, V> extends Map<K, V> {
44
45
  toJSON(): [K, V][];
46
+ #private;
45
47
  }
46
48
 
47
49
  // Compiler-injected runtime symbols (for Ripple component development)
@@ -74,17 +76,17 @@ export type Tracked<V> = { '#v': V };
74
76
  export type Props<K extends PropertyKey = any, V = unknown> = Record<K, V>;
75
77
  export type PropsWithExtras<T extends object> = Props & T & Record<string, unknown>;
76
78
  export type PropsWithChildren<T extends object = {}> =
77
- Expand<Omit<Props, 'children'> & { children: Component } & T>;
79
+ Expand<Omit<Props, 'children'> & { children: Component } & T>;
78
80
 
79
81
  type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
80
82
 
81
83
  type PickKeys<T, K extends readonly (keyof T)[]> =
82
- { [I in keyof K]: Tracked<T[K[I] & keyof T]> };
84
+ { [I in keyof K]: Tracked<T[K[I] & keyof T]> };
83
85
 
84
86
  type RestKeys<T, K extends readonly (keyof T)[]> = Expand<Omit<T, K[number]>>;
85
87
 
86
88
  type SplitResult<T extends Props, K extends readonly (keyof T)[]> =
87
- [...PickKeys<T, K>, Tracked<RestKeys<T, K>>];
89
+ [...PickKeys<T, K>, Tracked<RestKeys<T, K>>];
88
90
 
89
91
  export declare function track<V>(value?: V | (() => V), get?: (v: V) => V, set?: (next: V, prev: V) => V): Tracked<V>;
90
92
 
@@ -129,33 +131,38 @@ export function on(
129
131
  ): () => void;
130
132
 
131
133
  export type TrackedObjectShallow<T> = {
132
- [K in keyof T]: T[K] | Tracked<T[K]>;
134
+ [K in keyof T]: T[K] | Tracked<T[K]>;
133
135
  };
134
136
 
135
137
  export type TrackedObjectDeep<T> =
136
- T extends string | number | boolean | null | undefined | symbol | bigint
137
- ? T | Tracked<T>
138
- : T extends TrackedArray<infer U>
139
- ? TrackedArray<U> | Tracked<TrackedArray<U>>
140
- : T extends TrackedSet<infer U>
141
- ? TrackedSet<U> | Tracked<TrackedSet<U>>
142
- : T extends TrackedMap<infer K, infer V>
143
- ? TrackedMap<K, V> | Tracked<TrackedMap<K, V>>
144
- : T extends Array<infer U>
145
- ? Array<TrackedObjectDeep<U>> | Tracked<Array<TrackedObjectDeep<U>>>
146
- : T extends Set<infer U>
147
- ? Set<TrackedObjectDeep<U>> | Tracked<Set<TrackedObjectDeep<U>>>
148
- : T extends Map<infer K, infer V>
149
- ? Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>> |
150
- Tracked<Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>>>
151
- : T extends object
152
- ? { [K in keyof T]: TrackedObjectDeep<T[K]> | Tracked<TrackedObjectDeep<T[K]>> }
153
- : T | Tracked<T>;
138
+ T extends string | number | boolean | null | undefined | symbol | bigint
139
+ ? T | Tracked<T>
140
+ : T extends TrackedArray<infer U>
141
+ ? TrackedArray<U> | Tracked<TrackedArray<U>>
142
+ : T extends TrackedSet<infer U>
143
+ ? TrackedSet<U> | Tracked<TrackedSet<U>>
144
+ : T extends TrackedMap<infer K, infer V>
145
+ ? TrackedMap<K, V> | Tracked<TrackedMap<K, V>>
146
+ : T extends Array<infer U>
147
+ ? Array<TrackedObjectDeep<U>> | Tracked<Array<TrackedObjectDeep<U>>>
148
+ : T extends Set<infer U>
149
+ ? Set<TrackedObjectDeep<U>> | Tracked<Set<TrackedObjectDeep<U>>>
150
+ : T extends Map<infer K, infer V>
151
+ ? Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>> |
152
+ Tracked<Map<TrackedObjectDeep<K>, TrackedObjectDeep<V>>>
153
+ : T extends object
154
+ ? { [K in keyof T]: TrackedObjectDeep<T[K]> | Tracked<TrackedObjectDeep<T[K]>> }
155
+ : T | Tracked<T>;
154
156
 
155
157
  export type TrackedObject<T extends object> = T & {};
156
158
 
157
159
  export interface TrackedObjectConstructor {
158
- new <T extends object>(obj: T): TrackedObject<T>;
160
+ new <T extends object>(obj: T): TrackedObject<T>;
159
161
  }
160
162
 
161
163
  export declare const TrackedObject: TrackedObjectConstructor;
164
+
165
+ export class SvelteDate extends Date {
166
+ constructor(...params: any[]);
167
+ #private;
168
+ }