ripple 0.2.115 → 0.2.116

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.115",
6
+ "version": "0.2.116",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -167,6 +167,22 @@ function RipplePlugin(config) {
167
167
  }
168
168
  }
169
169
 
170
+ // Check if this is #Map or #Set
171
+ if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
172
+ const charAfter = this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
173
+ if (charAfter === 40) { // ( character
174
+ this.pos += 4; // consume '#Map'
175
+ return this.finishToken(tt.name, '#Map');
176
+ }
177
+ }
178
+ if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
179
+ const charAfter = this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
180
+ if (charAfter === 40) { // ( character
181
+ this.pos += 4; // consume '#Set'
182
+ return this.finishToken(tt.name, '#Set');
183
+ }
184
+ }
185
+
170
186
  // Check if this is #server
171
187
  if (this.input.slice(this.pos, this.pos + 7) === '#server') {
172
188
  // Check that next char after 'server' is whitespace, {, . (dot), or EOF
@@ -186,6 +202,27 @@ function RipplePlugin(config) {
186
202
  return this.finishToken(tt.name, '#server');
187
203
  }
188
204
  }
205
+
206
+ // Check if this is an invalid #Identifier pattern
207
+ // Valid patterns: #[, #{, #Map(, #Set(, #server
208
+ // If we see # followed by an uppercase letter that isn't Map or Set, it's an error
209
+ if (nextChar >= 65 && nextChar <= 90) { // A-Z
210
+ // Extract the identifier name
211
+ let identEnd = this.pos + 1;
212
+ while (identEnd < this.input.length) {
213
+ const ch = this.input.charCodeAt(identEnd);
214
+ if ((ch >= 65 && ch <= 90) || (ch >= 97 && ch <= 122) || (ch >= 48 && ch <= 57) || ch === 95) {
215
+ // A-Z, a-z, 0-9, _
216
+ identEnd++;
217
+ } else {
218
+ break;
219
+ }
220
+ }
221
+ const identName = this.input.slice(this.pos + 1, identEnd);
222
+ if (identName !== 'Map' && identName !== 'Set') {
223
+ this.raise(this.pos, `Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`);
224
+ }
225
+ }
189
226
  }
190
227
  }
191
228
  if (code === 64) {
@@ -391,6 +428,12 @@ function RipplePlugin(config) {
391
428
  return this.finishNode(node, 'ServerIdentifier');
392
429
  }
393
430
 
431
+ // Check if this is #Map( or #Set(
432
+ if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
433
+ const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
434
+ return this.parseTrackedCollectionExpression(type);
435
+ }
436
+
394
437
  // Check if this is a tuple literal starting with #[
395
438
  if (this.type === tt.bracketL && this.value === '#[') {
396
439
  return this.parseTrackedArrayExpression();
@@ -449,6 +492,42 @@ function RipplePlugin(config) {
449
492
  return this.finishNode(node, 'ServerBlock');
450
493
  }
451
494
 
495
+ /**
496
+ * Parse `#Map(...)` or `#Set(...)` syntax for tracked collections
497
+ * Creates a TrackedMap or TrackedSet node with the arguments property
498
+ * @param {string} type - Either 'TrackedMap' or 'TrackedSet'
499
+ * @returns {any} TrackedMap or TrackedSet node
500
+ */
501
+ parseTrackedCollectionExpression(type) {
502
+ const node = this.startNode();
503
+ this.next(); // consume '#Map' or '#Set'
504
+ this.expect(tt.parenL); // expect '('
505
+
506
+ node.arguments = [];
507
+
508
+ // Parse arguments similar to function call arguments
509
+ let first = true;
510
+ while (!this.eat(tt.parenR)) {
511
+ if (!first) {
512
+ this.expect(tt.comma);
513
+ if (this.afterTrailingComma(tt.parenR)) break;
514
+ } else {
515
+ first = false;
516
+ }
517
+
518
+ if (this.type === tt.ellipsis) {
519
+ // Spread argument
520
+ const arg = this.parseSpread();
521
+ node.arguments.push(arg);
522
+ } else {
523
+ // Regular argument
524
+ node.arguments.push(this.parseMaybeAssign(false));
525
+ }
526
+ }
527
+
528
+ return this.finishNode(node, type);
529
+ }
530
+
452
531
  parseTrackedArrayExpression() {
453
532
  const node = this.startNode();
454
533
  this.next(); // consume the '#['
@@ -350,11 +350,9 @@ const visitors = {
350
350
  context.state.imports.add(`import { TrackedArray } from 'ripple'`);
351
351
  }
352
352
 
353
- return b.new(
354
- b.call(
355
- b.member(b.id('TrackedArray'), b.id('from')),
356
- node.elements.map((el) => context.visit(el)),
357
- ),
353
+ return b.call(
354
+ b.member(b.id('TrackedArray'), b.id('from')),
355
+ ...node.elements.map((el) => context.visit(el)),
358
356
  );
359
357
  }
360
358
 
@@ -384,6 +382,48 @@ const visitors = {
384
382
  );
385
383
  },
386
384
 
385
+ TrackedMapExpression(node, context) {
386
+ if (context.state.to_ts) {
387
+ if (!context.state.imports.has(`import { TrackedMap } from 'ripple'`)) {
388
+ context.state.imports.add(`import { TrackedMap } from 'ripple'`);
389
+ }
390
+
391
+ const calleeId = b.id('TrackedMap');
392
+ // Preserve location from original node for Volar mapping
393
+ calleeId.loc = node.loc;
394
+ // Add metadata for Volar mapping - map "TrackedMap" identifier to "#Map" in source
395
+ calleeId.metadata = { tracked_shorthand: '#Map' };
396
+ return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
397
+ }
398
+
399
+ return b.call(
400
+ '_$_.tracked_map',
401
+ b.id('__block'),
402
+ ...node.arguments.map((arg) => context.visit(arg)),
403
+ );
404
+ },
405
+
406
+ TrackedSetExpression(node, context) {
407
+ if (context.state.to_ts) {
408
+ if (!context.state.imports.has(`import { TrackedSet } from 'ripple'`)) {
409
+ context.state.imports.add(`import { TrackedSet } from 'ripple'`);
410
+ }
411
+
412
+ const calleeId = b.id('TrackedSet');
413
+ // Preserve location from original node for Volar mapping
414
+ calleeId.loc = node.loc;
415
+ // Add metadata for Volar mapping - map "TrackedSet" identifier to "#Set" in source
416
+ calleeId.metadata = { tracked_shorthand: '#Set' };
417
+ return b.new(calleeId, ...node.arguments.map((arg) => context.visit(arg)));
418
+ }
419
+
420
+ return b.call(
421
+ '_$_.tracked_set',
422
+ b.id('__block'),
423
+ ...node.arguments.map((arg) => context.visit(arg)),
424
+ );
425
+ },
426
+
387
427
  TrackedExpression(node, context) {
388
428
  return b.call('_$_.get', context.visit(node.argument));
389
429
  },
@@ -459,7 +499,7 @@ const visitors = {
459
499
  return {
460
500
  ...node,
461
501
  id: { ...node.id, name: capitalizedName },
462
- init: node.init ? context.visit(node.init) : null
502
+ init: node.init ? context.visit(node.init) : null,
463
503
  };
464
504
  }
465
505
  }
@@ -1517,12 +1557,18 @@ function transform_ts_child(node, context) {
1517
1557
  };
1518
1558
  }
1519
1559
 
1520
- const jsxElement = b.jsx_element(opening_type, attributes, children, node.selfClosing, closing_type);
1560
+ const jsxElement = b.jsx_element(
1561
+ opening_type,
1562
+ attributes,
1563
+ children,
1564
+ node.selfClosing,
1565
+ closing_type,
1566
+ );
1521
1567
  // Preserve metadata from Element node for mapping purposes
1522
1568
  if (node.metadata && (node.metadata.ts_name || node.metadata.original_name)) {
1523
1569
  jsxElement.metadata = {
1524
1570
  ts_name: node.metadata.ts_name,
1525
- original_name: node.metadata.original_name
1571
+ original_name: node.metadata.original_name,
1526
1572
  };
1527
1573
  }
1528
1574
  state.init.push(b.stmt(jsxElement));
@@ -167,18 +167,23 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
167
167
  // Only collect tokens from nodes with .loc (skip synthesized nodes like children attribute)
168
168
  if (node.type === 'Identifier' && node.name) {
169
169
  if (node.loc) {
170
- // Check if this identifier was capitalized (reverse lookup)
171
- const originalName = reverseCapitalizedNames.get(node.name);
172
- if (originalName) {
173
- // This is a capitalized name in generated code, map to lowercase in source
174
- tokens.push({ source: originalName, generated: node.name });
170
+ // Check if this identifier has tracked_shorthand metadata (e.g., TrackedMap -> #Map)
171
+ if (node.metadata?.tracked_shorthand) {
172
+ tokens.push({ source: node.metadata.tracked_shorthand, generated: node.name });
175
173
  } else {
176
- // Check if this identifier should be capitalized (forward lookup)
177
- const capitalizedName = capitalizedNames.get(node.name);
178
- if (capitalizedName) {
179
- tokens.push({ source: node.name, generated: capitalizedName });
174
+ // Check if this identifier was capitalized (reverse lookup)
175
+ const originalName = reverseCapitalizedNames.get(node.name);
176
+ if (originalName) {
177
+ // This is a capitalized name in generated code, map to lowercase in source
178
+ tokens.push({ source: originalName, generated: node.name });
180
179
  } else {
181
- tokens.push(node.name);
180
+ // Check if this identifier should be capitalized (forward lookup)
181
+ const capitalizedName = capitalizedNames.get(node.name);
182
+ if (capitalizedName) {
183
+ tokens.push({ source: node.name, generated: capitalizedName });
184
+ } else {
185
+ tokens.push(node.name);
186
+ }
182
187
  }
183
188
  }
184
189
  }
@@ -78,6 +78,22 @@ export interface TrackedObjectExpression extends Omit<ObjectExpression, 'type'>
78
78
  properties: (Property | SpreadElement)[];
79
79
  }
80
80
 
81
+ /**
82
+ * Tracked Map expression node
83
+ */
84
+ export interface TrackedMapExpression extends Omit<Node, 'type'> {
85
+ type: 'TrackedMapExpression';
86
+ arguments: (Expression | SpreadElement)[];
87
+ }
88
+
89
+ /**
90
+ * Tracked Set expression node
91
+ */
92
+ export interface TrackedSetExpression extends Omit<Node, 'type'> {
93
+ type: 'TrackedSetExpression';
94
+ arguments: (Expression | SpreadElement)[];
95
+ }
96
+
81
97
  /**
82
98
  * Ripple component node
83
99
  */
@@ -1,10 +1,10 @@
1
- /** @import { Block, Tracked } from '#client' */
1
+ /** @import { Block } from '#client' */
2
2
 
3
- import { destroy_block, effect, render, root } from './internal/client/blocks.js';
4
- import { handle_root_events, on } from './internal/client/events.js';
3
+ import { destroy_block, root } from './internal/client/blocks.js';
4
+ import { handle_root_events } from './internal/client/events.js';
5
5
  import { init_operations } from './internal/client/operations.js';
6
- import { active_block, get, set, tick } from './internal/client/runtime.js';
7
- import { create_anchor, is_array, is_tracked_object } from './internal/client/utils.js';
6
+ import { active_block } from './internal/client/runtime.js';
7
+ import { create_anchor } from './internal/client/utils.js';
8
8
  import { remove_ssr_css } from './internal/client/css.js';
9
9
 
10
10
  // Re-export JSX runtime functions for jsxImportSource: "ripple"
@@ -78,182 +78,16 @@ export { ref_prop as createRefKey } from './internal/client/runtime.js';
78
78
 
79
79
  export { on } from './internal/client/events.js';
80
80
 
81
- /**
82
- * @param {string} value
83
- */
84
- function to_number(value) {
85
- return value === '' ? null : +value;
86
- }
87
-
88
- /**
89
- * @param {HTMLInputElement} input
90
- */
91
- function is_numberlike_input(input) {
92
- var type = input.type;
93
- return type === 'number' || type === 'range';
94
- }
95
-
96
- /** @param {HTMLOptionElement} option */
97
- function get_option_value(option) {
98
- return option.value;
99
- }
100
-
101
- /**
102
- * Selects the correct option(s) (depending on whether this is a multiple select)
103
- * @template V
104
- * @param {HTMLSelectElement} select
105
- * @param {V} value
106
- * @param {boolean} mounting
107
- */
108
- function select_option(select, value, mounting = false) {
109
- if (select.multiple) {
110
- // If value is null or undefined, keep the selection as is
111
- if (value == undefined) {
112
- return;
113
- }
114
-
115
- // If not an array, warn and keep the selection as is
116
- if (!is_array(value)) {
117
- // TODO
118
- }
119
-
120
- // Otherwise, update the selection
121
- for (var option of select.options) {
122
- option.selected = /** @type {string[]} */ (value).includes(get_option_value(option));
123
- }
124
-
125
- return;
126
- }
127
-
128
- for (option of select.options) {
129
- var option_value = get_option_value(option);
130
- if (option_value === value) {
131
- option.selected = true;
132
- return;
133
- }
134
- }
135
-
136
- if (!mounting || value !== undefined) {
137
- select.selectedIndex = -1; // no option should be selected
138
- }
139
- }
140
-
141
- /**
142
- * @param {unknown} maybe_tracked
143
- * @returns {(node: HTMLInputElement | HTMLSelectElement) => void}
144
- */
145
- export function bindValue(maybe_tracked) {
146
- if (!is_tracked_object(maybe_tracked)) {
147
- throw new TypeError('bindValue() argument is not a tracked object');
148
- }
149
-
150
- var block = /** @type {Block} */ (active_block);
151
- var tracked = /** @type {Tracked} */ (maybe_tracked);
152
-
153
- return (node) => {
154
- var clear_event;
155
-
156
- if (node.tagName === 'SELECT') {
157
- var select = /** @type {HTMLSelectElement} */ (node);
158
- var mounting = true;
159
-
160
- clear_event = on(select, 'change', async () => {
161
- var query = ':checked';
162
- /** @type {unknown} */
163
- var value;
164
-
165
- if (select.multiple) {
166
- value = [].map.call(select.querySelectorAll(query), get_option_value);
167
- } else {
168
- /** @type {HTMLOptionElement | null} */
169
- var selected_option =
170
- select.querySelector(query) ??
171
- // will fall back to first non-disabled option if no option is selected
172
- select.querySelector('option:not([disabled])');
173
- value = selected_option && get_option_value(selected_option);
174
- }
175
-
176
- set(tracked, value, block);
177
- });
178
-
179
- effect(() => {
180
- var value = get(tracked);
181
- select_option(select, value, mounting);
182
-
183
- // Mounting and value undefined -> take selection from dom
184
- if (mounting && value === undefined) {
185
- /** @type {HTMLOptionElement | null} */
186
- var selected_option = select.querySelector(':checked');
187
- if (selected_option !== null) {
188
- value = get_option_value(selected_option);
189
- set(tracked, value, block);
190
- }
191
- }
192
-
193
- mounting = false;
194
- });
195
- } else {
196
- var input = /** @type {HTMLInputElement} */ (node);
197
-
198
- clear_event = on(input, 'input', async () => {
199
- /** @type {any} */
200
- var value = input.value;
201
- value = is_numberlike_input(input) ? to_number(value) : value;
202
- set(tracked, value, block);
203
-
204
- await tick();
205
-
206
- if (value !== (value = get(tracked))) {
207
- var start = input.selectionStart;
208
- var end = input.selectionEnd;
209
- input.value = value ?? '';
210
-
211
- // Restore selection
212
- if (end !== null) {
213
- input.selectionStart = start;
214
- input.selectionEnd = Math.min(end, input.value.length);
215
- }
216
- }
217
- });
218
-
219
- render(() => {
220
- var value = get(tracked);
221
-
222
- if (is_numberlike_input(input) && value === to_number(input.value)) {
223
- return;
224
- }
225
-
226
- if (input.type === 'date' && !value && !input.value) {
227
- return;
228
- }
229
-
230
- if (value !== input.value) {
231
- input.value = value ?? '';
232
- }
233
- });
234
-
235
- return clear_event;
236
- }
237
- };
238
- }
239
-
240
- /**
241
- * @param {unknown} maybe_tracked
242
- * @returns {(node: HTMLInputElement) => void}
243
- */
244
- export function bindChecked(maybe_tracked) {
245
- if (!is_tracked_object(maybe_tracked)) {
246
- throw new TypeError('bindChecked() argument is not a tracked object');
247
- }
248
-
249
- const block = /** @type {any} */ (active_block);
250
- const tracked = /** @type {Tracked<any>} */ (maybe_tracked);
251
-
252
- return (input) => {
253
- const clear_event = on(input, 'change', () => {
254
- set(tracked, input.checked, block);
255
- });
256
-
257
- return clear_event;
258
- };
259
- }
81
+ export {
82
+ bindValue,
83
+ bindChecked,
84
+ bindClientWidth,
85
+ bindClientHeight,
86
+ bindContentRect,
87
+ bindContentBoxSize,
88
+ bindBorderBoxSize,
89
+ bindDevicePixelContentBoxSize,
90
+ bindInnerHTML,
91
+ bindInnerText,
92
+ bindTextContent,
93
+ } from './internal/client/bindings.js';