ripple 0.1.1 → 0.2.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +56 -24
  3. package/src/ai.js +292 -0
  4. package/src/compiler/errors.js +26 -0
  5. package/src/compiler/index.js +26 -0
  6. package/src/compiler/phases/1-parse/index.js +543 -0
  7. package/src/compiler/phases/1-parse/style.js +566 -0
  8. package/src/compiler/phases/2-analyze/index.js +509 -0
  9. package/src/compiler/phases/2-analyze/prune.js +572 -0
  10. package/src/compiler/phases/3-transform/index.js +1572 -0
  11. package/src/compiler/phases/3-transform/segments.js +91 -0
  12. package/src/compiler/phases/3-transform/stylesheet.js +372 -0
  13. package/src/compiler/scope.js +421 -0
  14. package/src/compiler/utils.js +552 -0
  15. package/src/constants.js +4 -0
  16. package/src/jsx-runtime.d.ts +94 -0
  17. package/src/jsx-runtime.js +46 -0
  18. package/src/runtime/array.js +215 -0
  19. package/src/runtime/index.js +39 -0
  20. package/src/runtime/internal/client/blocks.js +247 -0
  21. package/src/runtime/internal/client/constants.js +23 -0
  22. package/src/runtime/internal/client/events.js +223 -0
  23. package/src/runtime/internal/client/for.js +388 -0
  24. package/src/runtime/internal/client/if.js +35 -0
  25. package/src/runtime/internal/client/index.js +53 -0
  26. package/src/runtime/internal/client/operations.js +72 -0
  27. package/src/runtime/internal/client/portal.js +33 -0
  28. package/src/runtime/internal/client/render.js +156 -0
  29. package/src/runtime/internal/client/runtime.js +909 -0
  30. package/src/runtime/internal/client/template.js +51 -0
  31. package/src/runtime/internal/client/try.js +139 -0
  32. package/src/runtime/internal/client/utils.js +16 -0
  33. package/src/utils/ast.js +214 -0
  34. package/src/utils/builders.js +733 -0
  35. package/src/utils/patterns.js +23 -0
  36. package/src/utils/sanitize_template_string.js +7 -0
  37. package/test-mappings.js +0 -0
  38. package/types/index.d.ts +2 -0
  39. package/.npmignore +0 -2
  40. package/History.md +0 -3
  41. package/Readme.md +0 -151
  42. package/lib/exec/index.js +0 -60
  43. package/ripple.js +0 -645
@@ -0,0 +1,552 @@
1
+ import { build_assignment_value } from '../utils/ast.js';
2
+ import * as b from '../utils/builders.js';
3
+
4
+ const regex_return_characters = /\r/g;
5
+
6
+ const RESERVED_WORDS = [
7
+ 'arguments',
8
+ 'await',
9
+ 'break',
10
+ 'case',
11
+ 'catch',
12
+ 'class',
13
+ 'const',
14
+ 'continue',
15
+ 'debugger',
16
+ 'default',
17
+ 'delete',
18
+ 'do',
19
+ 'else',
20
+ 'enum',
21
+ 'eval',
22
+ 'export',
23
+ 'extends',
24
+ 'false',
25
+ 'finally',
26
+ 'for',
27
+ 'function',
28
+ 'if',
29
+ 'implements',
30
+ 'import',
31
+ 'in',
32
+ 'instanceof',
33
+ 'interface',
34
+ 'let',
35
+ 'new',
36
+ 'null',
37
+ 'package',
38
+ 'private',
39
+ 'protected',
40
+ 'public',
41
+ 'return',
42
+ 'static',
43
+ 'super',
44
+ 'switch',
45
+ 'this',
46
+ 'throw',
47
+ 'true',
48
+ 'try',
49
+ 'typeof',
50
+ 'var',
51
+ 'void',
52
+ 'while',
53
+ 'with',
54
+ 'yield'
55
+ ];
56
+
57
+ export function is_reserved(word) {
58
+ return RESERVED_WORDS.includes(word);
59
+ }
60
+
61
+ /**
62
+ * Attributes that are boolean, i.e. they are present or not present.
63
+ */
64
+ const DOM_BOOLEAN_ATTRIBUTES = [
65
+ 'allowfullscreen',
66
+ 'async',
67
+ 'autofocus',
68
+ 'autoplay',
69
+ 'checked',
70
+ 'controls',
71
+ 'default',
72
+ 'disabled',
73
+ 'formnovalidate',
74
+ 'hidden',
75
+ 'indeterminate',
76
+ 'inert',
77
+ 'ismap',
78
+ 'loop',
79
+ 'multiple',
80
+ 'muted',
81
+ 'nomodule',
82
+ 'novalidate',
83
+ 'open',
84
+ 'playsinline',
85
+ 'readonly',
86
+ 'required',
87
+ 'reversed',
88
+ 'seamless',
89
+ 'selected',
90
+ 'webkitdirectory',
91
+ 'defer',
92
+ 'disablepictureinpicture',
93
+ 'disableremoteplayback'
94
+ ];
95
+
96
+ export function is_boolean_attribute(name) {
97
+ return DOM_BOOLEAN_ATTRIBUTES.includes(name);
98
+ }
99
+
100
+ const DOM_PROPERTIES = [
101
+ ...DOM_BOOLEAN_ATTRIBUTES,
102
+ 'formNoValidate',
103
+ 'isMap',
104
+ 'noModule',
105
+ 'playsInline',
106
+ 'readOnly',
107
+ 'value',
108
+ 'volume',
109
+ 'defaultValue',
110
+ 'defaultChecked',
111
+ 'srcObject',
112
+ 'noValidate',
113
+ 'allowFullscreen',
114
+ 'disablePictureInPicture',
115
+ 'disableRemotePlayback'
116
+ ];
117
+
118
+ export function is_dom_property(name) {
119
+ return DOM_PROPERTIES.includes(name);
120
+ }
121
+
122
+ /** List of Element events that will be delegated */
123
+ const DELEGATED_EVENTS = [
124
+ 'beforeinput',
125
+ 'click',
126
+ 'change',
127
+ 'dblclick',
128
+ 'contextmenu',
129
+ 'focusin',
130
+ 'focusout',
131
+ 'input',
132
+ 'keydown',
133
+ 'keyup',
134
+ 'mousedown',
135
+ 'mousemove',
136
+ 'mouseout',
137
+ 'mouseover',
138
+ 'mouseup',
139
+ 'pointerdown',
140
+ 'pointermove',
141
+ 'pointerout',
142
+ 'pointerover',
143
+ 'pointerup',
144
+ 'touchend',
145
+ 'touchmove',
146
+ 'touchstart'
147
+ ];
148
+
149
+ export function is_delegated(event_name) {
150
+ return DELEGATED_EVENTS.includes(event_name);
151
+ }
152
+
153
+ const PASSIVE_EVENTS = ['touchstart', 'touchmove'];
154
+
155
+ export function is_passive_event(name) {
156
+ return PASSIVE_EVENTS.includes(name);
157
+ }
158
+
159
+ export function is_event_attribute(attr) {
160
+ return attr.startsWith('on') && attr.length > 2 && attr[2] === attr[2].toUpperCase();
161
+ }
162
+
163
+ const unhoisted = { hoisted: false };
164
+
165
+ export function get_delegated_event(event_name, handler, state) {
166
+ // Handle delegated event handlers. Bail out if not a delegated event.
167
+ if (!handler || !is_delegated(event_name)) {
168
+ return null;
169
+ }
170
+
171
+ /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
172
+ let target_function = null;
173
+ let binding = null;
174
+
175
+ if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
176
+ target_function = handler;
177
+ } else if (handler.type === 'Identifier') {
178
+ binding = state.scope.get(handler.name);
179
+
180
+ if (state.analysis.module.scope.references.has(handler.name)) {
181
+ // If a binding with the same name is referenced in the module scope (even if not declared there), bail out
182
+ return unhoisted;
183
+ }
184
+
185
+ if (binding != null) {
186
+ for (const { path } of binding.references) {
187
+ const parent = path.at(-1);
188
+ if (parent === undefined) return unhoisted;
189
+
190
+ const grandparent = path.at(-2);
191
+
192
+ /** @type {AST.RegularElement | null} */
193
+ let element = null;
194
+ /** @type {string | null} */
195
+ let event_name = null;
196
+ if (
197
+ parent.type === 'ExpressionTag' &&
198
+ grandparent?.type === 'Attribute' &&
199
+ is_event_attribute(grandparent)
200
+ ) {
201
+ element = /** @type {AST.RegularElement} */ (path.at(-3));
202
+ const attribute = /** @type {AST.Attribute} */ (grandparent);
203
+ event_name = get_attribute_event_name(attribute.name);
204
+ }
205
+
206
+ if (element && event_name) {
207
+ if (
208
+ element.type !== 'Element' ||
209
+ element.metadata.has_spread ||
210
+ !is_delegated(event_name)
211
+ ) {
212
+ return unhoisted;
213
+ }
214
+ } else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
215
+ return unhoisted;
216
+ }
217
+ }
218
+ }
219
+
220
+ // If the binding is exported, bail out
221
+ if (state.analysis.exports.find((node) => node.name === handler.name)) {
222
+ return unhoisted;
223
+ }
224
+
225
+ if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
226
+ const binding_type = binding.initial.type;
227
+
228
+ if (
229
+ binding_type === 'ArrowFunctionExpression' ||
230
+ binding_type === 'FunctionDeclaration' ||
231
+ binding_type === 'FunctionExpression'
232
+ ) {
233
+ target_function = binding.initial;
234
+ }
235
+ }
236
+ }
237
+
238
+ // If we can't find a function, or the function has multiple parameters, bail out
239
+ if (target_function == null || target_function.params.length > 1) {
240
+ return unhoisted;
241
+ }
242
+
243
+ const visited_references = new Set();
244
+ const scope = target_function.metadata.scope;
245
+ for (const [reference] of scope.references) {
246
+ // Bail out if the arguments keyword is used or $host is referenced
247
+ if (reference === 'arguments') return unhoisted;
248
+
249
+ const binding = scope.get(reference);
250
+ const local_binding = state.scope.get(reference);
251
+
252
+ // If we are referencing a binding that is shadowed in another scope then bail out.
253
+ if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
254
+ return unhoisted;
255
+ }
256
+
257
+ if (
258
+ binding !== null &&
259
+ // Bail out if the the binding is a rest param
260
+ (binding.declaration_kind === 'rest_param' || // or any normal not reactive bindings that are mutated.
261
+ // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
262
+ (binding.kind === 'normal' && binding.updated))
263
+ ) {
264
+ return unhoisted;
265
+ }
266
+ visited_references.add(reference);
267
+ }
268
+
269
+ return { hoisted: true, function: target_function };
270
+ }
271
+
272
+ function get_hoisted_params(node, context) {
273
+ const scope = context.state.scope;
274
+
275
+ /** @type {Identifier[]} */
276
+ const params = [];
277
+
278
+ /**
279
+ * We only want to push if it's not already present to avoid name clashing
280
+ * @param {Identifier} id
281
+ */
282
+ function push_unique(id) {
283
+ if (!params.find((param) => param.name === id.name)) {
284
+ params.push(id);
285
+ }
286
+ }
287
+
288
+ for (const [reference] of scope.references) {
289
+ let binding = scope.get(reference);
290
+
291
+ if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) {
292
+ if (binding.kind === 'prop') {
293
+ debugger;
294
+ } else if (
295
+ // imports don't need to be hoisted
296
+ binding.declaration_kind !== 'import'
297
+ ) {
298
+ // create a copy to remove start/end tags which would mess up source maps
299
+ push_unique(b.id(binding.node.name));
300
+ }
301
+ }
302
+ }
303
+ return params;
304
+ }
305
+
306
+ export function build_hoisted_params(node, context) {
307
+ const hoisted_params = get_hoisted_params(node, context);
308
+ node.metadata.hoisted_params = hoisted_params;
309
+
310
+ /** @type {Pattern[]} */
311
+ const params = [];
312
+
313
+ if (node.params.length === 0) {
314
+ if (hoisted_params.length > 0) {
315
+ // For the event object
316
+ params.push(b.id(context.state.scope.generate('_')));
317
+ }
318
+ } else {
319
+ for (const param of node.params) {
320
+ params.push(/** @type {Pattern} */ (context.visit(param)));
321
+ }
322
+ }
323
+
324
+ params.push(...hoisted_params, b.id('__block'));
325
+ return params;
326
+ }
327
+
328
+ export function is_inside_component(context) {
329
+ for (let i = context.path.length - 1; i >= 0; i -= 1) {
330
+ const context_node = context.path[i];
331
+ const type = context_node.type;
332
+
333
+ if (
334
+ type === 'FunctionExpression' ||
335
+ type === 'ArrowFunctionExpression' ||
336
+ type === 'FunctionDeclaration'
337
+ ) {
338
+ return false;
339
+ }
340
+ if (type === 'Component') {
341
+ return true;
342
+ }
343
+ }
344
+ return false;
345
+ }
346
+
347
+ export function is_inside_call_expression(context) {
348
+ for (let i = context.path.length - 1; i >= 0; i -= 1) {
349
+ const context_node = context.path[i];
350
+ const type = context_node.type;
351
+
352
+ if (
353
+ type === 'FunctionExpression' ||
354
+ type === 'ArrowFunctionExpression' ||
355
+ type === 'FunctionDeclaration'
356
+ ) {
357
+ return false;
358
+ }
359
+ if (type === 'CallExpression') {
360
+ return true;
361
+ }
362
+ }
363
+ return false;
364
+ }
365
+
366
+ export function is_tracked_name(name) {
367
+ return (
368
+ typeof name === 'string' &&
369
+ name.startsWith('$') &&
370
+ name.length > 1 &&
371
+ name[1] !== '$' &&
372
+ name !== '$length'
373
+ );
374
+ }
375
+
376
+ export function is_svelte_import(callee, context) {
377
+ if (callee.type === 'Identifier') {
378
+ const binding = context.state.scope.get(callee.name);
379
+
380
+ return (
381
+ binding?.declaration_kind === 'import' &&
382
+ binding.initial.source.type === 'Literal' &&
383
+ binding.initial.source.value === 'ripple'
384
+ );
385
+ }
386
+
387
+ return false;
388
+ }
389
+
390
+ export function is_declared_within_component(node, context) {
391
+ const component = context.path.find((n) => n.type === 'Component');
392
+
393
+ if (node.type === 'Identifier' && component) {
394
+ const binding = context.state.scope.get(node.name);
395
+ const component_scope = context.state.scopes.get(component);
396
+
397
+ if (binding !== null && component_scope !== null) {
398
+ let scope = binding.scope;
399
+
400
+ while (scope !== null) {
401
+ if (scope === component_scope) {
402
+ return true;
403
+ }
404
+ scope = scope.parent;
405
+ }
406
+ }
407
+ }
408
+
409
+ return false;
410
+ }
411
+
412
+ function is_non_coercive_operator(operator) {
413
+ return ['=', '||=', '&&=', '??='].includes(operator);
414
+ }
415
+
416
+ export function visit_assignment_expression(node, context, build_assignment) {
417
+ if (
418
+ node.left.type === 'ArrayPattern' ||
419
+ node.left.type === 'ObjectPattern' ||
420
+ node.left.type === 'RestElement'
421
+ ) {
422
+ const value = /** @type {Expression} */ (context.visit(node.right));
423
+ const should_cache = value.type !== 'Identifier';
424
+ const rhs = should_cache ? b.id('$$value') : value;
425
+
426
+ let changed = false;
427
+
428
+ const assignments = extract_paths(node.left).map((path) => {
429
+ const value = path.expression?.(rhs);
430
+
431
+ let assignment = build_assignment('=', path.node, value, context);
432
+ if (assignment !== null) changed = true;
433
+
434
+ return (
435
+ assignment ??
436
+ b.assignment(
437
+ '=',
438
+ /** @type {Pattern} */ (context.visit(path.node)),
439
+ /** @type {Expression} */ (context.visit(value))
440
+ )
441
+ );
442
+ });
443
+
444
+ if (!changed) {
445
+ // No change to output -> nothing to transform -> we can keep the original assignment
446
+ return null;
447
+ }
448
+
449
+ const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
450
+ const sequence = b.sequence(assignments);
451
+
452
+ if (!is_standalone) {
453
+ // this is part of an expression, we need the sequence to end with the value
454
+ sequence.expressions.push(rhs);
455
+ }
456
+
457
+ if (should_cache) {
458
+ // the right hand side is a complex expression, wrap in an IIFE to cache it
459
+ const iife = b.arrow([rhs], sequence);
460
+
461
+ const iife_is_async =
462
+ is_expression_async(value) ||
463
+ assignments.some((assignment) => is_expression_async(assignment));
464
+
465
+ return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
466
+ }
467
+
468
+ return sequence;
469
+ }
470
+
471
+ if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
472
+ throw new Error(`Unexpected assignment type ${node.left.type}`);
473
+ }
474
+
475
+ return build_assignment(node.operator, node.left, node.right, context);
476
+ }
477
+
478
+ export function build_assignment(operator, left, right, context) {
479
+ let object = left;
480
+
481
+ while (object.type === 'MemberExpression') {
482
+ // @ts-expect-error
483
+ object = object.object;
484
+ }
485
+
486
+ if (object.type !== 'Identifier') {
487
+ return null;
488
+ }
489
+
490
+ const binding = context.state.scope.get(object.name);
491
+ if (!binding) return null;
492
+
493
+ const transform = binding.transform;
494
+
495
+ const path = context.path.map((node) => node.type);
496
+
497
+ // reassignment
498
+ if (object === left && transform?.assign) {
499
+ let value = /** @type {Expression} */ (
500
+ context.visit(build_assignment_value(operator, left, right))
501
+ );
502
+
503
+ return transform.assign(object, value);
504
+ }
505
+
506
+ // mutation
507
+ if (transform?.mutate) {
508
+ return transform.mutate(
509
+ object,
510
+ b.assignment(
511
+ operator,
512
+ /** @type {Pattern} */ (context.visit(left)),
513
+ /** @type {Expression} */ (context.visit(right))
514
+ )
515
+ );
516
+ }
517
+
518
+ return null;
519
+ }
520
+
521
+ const ATTR_REGEX = /[&"<]/g;
522
+ const CONTENT_REGEX = /[&<]/g;
523
+
524
+ export function escape_html(value, is_attr) {
525
+ const str = String(value ?? '');
526
+
527
+ const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
528
+ pattern.lastIndex = 0;
529
+
530
+ let escaped = '';
531
+ let last = 0;
532
+
533
+ while (pattern.test(str)) {
534
+ const i = pattern.lastIndex - 1;
535
+ const ch = str[i];
536
+ escaped += str.substring(last, i) + (ch === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
537
+ last = i + 1;
538
+ }
539
+
540
+ return escaped + str.substring(last);
541
+ }
542
+
543
+ export function hash(str) {
544
+ str = str.replace(regex_return_characters, '');
545
+ let hash = 5381;
546
+ let i = str.length;
547
+
548
+ while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
549
+ return (hash >>> 0).toString(36);
550
+ }
551
+
552
+
@@ -0,0 +1,4 @@
1
+
2
+ export const TEMPLATE_FRAGMENT = 1;
3
+ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
4
+ export const IS_CONTROLLED = 1 << 2;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Ripple JSX Runtime Type Definitions
3
+ * Ripple components are imperative and don't return JSX elements
4
+ */
5
+
6
+ // Ripple components don't return JSX elements - they're imperative
7
+ export type ComponentType<P = {}> = (props: P) => void;
8
+
9
+ /**
10
+ * Create a JSX element (for elements with children)
11
+ * In Ripple, this doesn't return anything - components are imperative
12
+ */
13
+ export function jsx(
14
+ type: string | ComponentType<any>,
15
+ props?: any,
16
+ key?: string | number | null
17
+ ): void;
18
+
19
+ /**
20
+ * Create a JSX element with static children (optimization for multiple children)
21
+ * In Ripple, this doesn't return anything - components are imperative
22
+ */
23
+ export function jsxs(
24
+ type: string | ComponentType<any>,
25
+ props?: any,
26
+ key?: string | number | null
27
+ ): void;
28
+
29
+ /**
30
+ * JSX Fragment component
31
+ * In Ripple, fragments are imperative and don't return anything
32
+ */
33
+ export function Fragment(props: { $children?: any }): void;
34
+
35
+ // Base HTML attributes
36
+ interface HTMLAttributes {
37
+ class?: string;
38
+ className?: string;
39
+ id?: string;
40
+ style?: string | Record<string, string | number>;
41
+ onClick?: (event: MouseEvent) => void;
42
+ onInput?: (event: InputEvent) => void;
43
+ onChange?: (event: Event) => void;
44
+ $children?: any;
45
+ [key: string]: any;
46
+ }
47
+
48
+ // Global JSX namespace for TypeScript
49
+ declare global {
50
+ namespace JSX {
51
+ // In Ripple, JSX expressions don't return elements - they're imperative
52
+ type Element = void;
53
+
54
+ interface IntrinsicElements {
55
+ // HTML elements with basic attributes
56
+ div: HTMLAttributes;
57
+ span: HTMLAttributes;
58
+ p: HTMLAttributes;
59
+ h1: HTMLAttributes;
60
+ h2: HTMLAttributes;
61
+ h3: HTMLAttributes;
62
+ h4: HTMLAttributes;
63
+ h5: HTMLAttributes;
64
+ h6: HTMLAttributes;
65
+ button: HTMLAttributes & {
66
+ type?: 'button' | 'submit' | 'reset';
67
+ disabled?: boolean;
68
+ };
69
+ input: HTMLAttributes & {
70
+ type?: string;
71
+ value?: string | number;
72
+ placeholder?: string;
73
+ disabled?: boolean;
74
+ };
75
+ form: HTMLAttributes;
76
+ a: HTMLAttributes & {
77
+ href?: string;
78
+ target?: string;
79
+ };
80
+ img: HTMLAttributes & {
81
+ src?: string;
82
+ alt?: string;
83
+ width?: string | number;
84
+ height?: string | number;
85
+ };
86
+ // Add more as needed...
87
+ [elemName: string]: HTMLAttributes;
88
+ }
89
+
90
+ interface ElementChildrenAttribute {
91
+ $children: {};
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ripple JSX Runtime
3
+ * This module provides the JSX runtime functions that TypeScript will automatically import
4
+ * when using jsxImportSource: "ripple/jsx-runtime"
5
+ */
6
+
7
+ /**
8
+ * Create a JSX element (for elements with children)
9
+ * In Ripple, components don't return values - they imperatively render to the DOM
10
+ * @param {string | Function} type - Element type (tag name or component function)
11
+ * @param {object} props - Element properties
12
+ * @param {string} key - Element key (optional)
13
+ * @returns {void} Ripple components don't return anything
14
+ */
15
+ export function jsx(type, props, key) {
16
+ // Ripple components are imperative - they don't return JSX elements
17
+ // This is a placeholder for the actual Ripple rendering logic
18
+ if (typeof type === 'function') {
19
+ // Call the Ripple component function
20
+ type(props);
21
+ } else {
22
+ // Handle DOM elements
23
+ console.warn('DOM element rendering not implemented in jsx runtime:', type, props);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Create a JSX element with static children (optimization for multiple children)
29
+ * @param {string | Function} type - Element type (tag name or component function)
30
+ * @param {object} props - Element properties
31
+ * @param {string} key - Element key (optional)
32
+ * @returns {void} Ripple components don't return anything
33
+ */
34
+ export function jsxs(type, props, key) {
35
+ return jsx(type, props, key);
36
+ }
37
+
38
+ /**
39
+ * JSX Fragment component
40
+ * @param {object} props - Fragment props (should contain children)
41
+ * @returns {void} Ripple fragments don't return anything
42
+ */
43
+ export function Fragment(props) {
44
+ // Ripple fragments are imperative
45
+ console.warn('Fragment rendering not implemented in jsx runtime:', props);
46
+ }