ripple 0.2.103 → 0.2.104

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.103",
6
+ "version": "0.2.104",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -106,7 +106,7 @@ const visitors = {
106
106
  }
107
107
 
108
108
  if (
109
- is_reference(node, /** @type {Node} */ (parent)) &&
109
+ is_reference(node, /** @type {Node} */(parent)) &&
110
110
  node.tracked &&
111
111
  binding?.node !== node
112
112
  ) {
@@ -117,7 +117,7 @@ const visitors = {
117
117
  }
118
118
 
119
119
  if (
120
- is_reference(node, /** @type {Node} */ (parent)) &&
120
+ is_reference(node, /** @type {Node} */(parent)) &&
121
121
  node.tracked &&
122
122
  binding?.node !== node
123
123
  ) {
@@ -250,7 +250,10 @@ const visitors = {
250
250
  }
251
251
  const elements = [];
252
252
 
253
- context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1 });
253
+ // Track metadata for this component
254
+ const metadata = { await: false };
255
+
256
+ context.next({ ...context.state, elements, function_depth: context.state.function_depth + 1, metadata });
254
257
 
255
258
  const css = node.css;
256
259
 
@@ -259,6 +262,12 @@ const visitors = {
259
262
  prune_css(css, node);
260
263
  }
261
264
  }
265
+
266
+ // Store component metadata in analysis
267
+ context.state.analysis.component_metadata.push({
268
+ id: node.id.name,
269
+ async: metadata.await,
270
+ });
262
271
  },
263
272
 
264
273
  ForStatement(node, context) {
@@ -357,6 +366,11 @@ const visitors = {
357
366
  }
358
367
 
359
368
  if (node.pending) {
369
+ // Try/pending blocks indicate async operations
370
+ if (context.state.metadata?.await === false) {
371
+ context.state.metadata.await = true;
372
+ }
373
+
360
374
  node.metadata = {
361
375
  has_template: false,
362
376
  };
@@ -594,6 +608,7 @@ export function analyze(ast, filename) {
594
608
  ast,
595
609
  scope,
596
610
  scopes,
611
+ component_metadata: [],
597
612
  };
598
613
 
599
614
  walk(
@@ -99,15 +99,19 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
99
99
  /** @type {string[]} */
100
100
  const tokens = [];
101
101
 
102
+ // We have to visit everything in generated order to maintain correct indices
102
103
  walk(ast, null, {
103
- _(node, { next, visit }) {
104
+ _(node, { visit }) {
104
105
  // Collect key node types: Identifiers, Literals, and JSX Elements
105
106
  if (node.type === 'Identifier' && node.name) {
106
107
  tokens.push(node.name);
108
+ return; // Leaf node, don't traverse further
107
109
  } else if (node.type === 'JSXIdentifier' && node.name) {
108
110
  tokens.push(node.name);
111
+ return; // Leaf node, don't traverse further
109
112
  } else if (node.type === 'Literal' && node.raw) {
110
113
  tokens.push(node.raw);
114
+ return; // Leaf node, don't traverse further
111
115
  } else if (node.type === 'ImportDeclaration') {
112
116
  // Visit specifiers in source order
113
117
  if (node.specifiers) {
@@ -134,15 +138,15 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
134
138
  }
135
139
  return;
136
140
  } else if (node.type === 'ExportNamedDeclaration') {
137
- // Visit in source order: declaration, specifiers
138
- if (node.declaration) {
139
- visit(node.declaration);
140
- }
141
- if (node.specifiers) {
141
+ if (node.specifiers && node.specifiers.length > 0) {
142
142
  for (const specifier of node.specifiers) {
143
143
  visit(specifier);
144
144
  }
145
145
  }
146
+ if (node.declaration) {
147
+ // The declaration will be visited with proper ordering
148
+ visit(node.declaration);
149
+ }
146
150
  return;
147
151
  } else if (node.type === 'ExportDefaultDeclaration') {
148
152
  // Visit the declaration
@@ -153,6 +157,41 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
153
157
  } else if (node.type === 'ExportAllDeclaration') {
154
158
  // Nothing to visit (just source string)
155
159
  return;
160
+ } else if (node.type === 'JSXOpeningElement') {
161
+ // Visit name and attributes in source order
162
+ if (node.name) {
163
+ visit(node.name);
164
+ }
165
+ if (node.attributes) {
166
+ for (const attr of node.attributes) {
167
+ visit(attr);
168
+ }
169
+ }
170
+ return;
171
+ } else if (node.type === 'JSXAttribute') {
172
+ // Visit name and value in source order
173
+ if (node.name) {
174
+ visit(node.name);
175
+ }
176
+ if (node.value) {
177
+ visit(node.value);
178
+ }
179
+ return;
180
+ } else if (node.type === 'JSXSpreadAttribute') {
181
+ // Visit the spread argument
182
+ if (node.argument) {
183
+ visit(node.argument);
184
+ }
185
+ return;
186
+ } else if (node.type === 'JSXExpressionContainer') {
187
+ // Visit the expression inside {}
188
+ if (node.expression) {
189
+ visit(node.expression);
190
+ }
191
+ return;
192
+ } else if (node.type === 'JSXText') {
193
+ // Text content, no tokens to collect
194
+ return;
156
195
  } else if (node.type === 'JSXElement') {
157
196
  // Manually visit in source order: opening element, children, closing element
158
197
 
@@ -335,11 +374,18 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
335
374
  return;
336
375
  } else if (node.type === 'Property') {
337
376
  // Visit in source order: key, value
338
- if (node.key) {
339
- visit(node.key);
340
- }
341
- if (node.value) {
342
- visit(node.value);
377
+ // For shorthand properties ({ count }), key and value are the same node, only visit once
378
+ if (node.shorthand) {
379
+ if (node.value) {
380
+ visit(node.value);
381
+ }
382
+ } else {
383
+ if (node.key) {
384
+ visit(node.key);
385
+ }
386
+ if (node.value) {
387
+ visit(node.value);
388
+ }
343
389
  }
344
390
  return;
345
391
  } else if (node.type === 'ArrayExpression' || node.type === 'ArrayPattern') {
@@ -459,9 +505,482 @@ export function convert_source_map_to_mappings(ast, source, generated_code) {
459
505
  visit(node.value);
460
506
  }
461
507
  return;
508
+ } else if (node.type === 'SequenceExpression') {
509
+ // Visit expressions in order
510
+ if (node.expressions) {
511
+ for (const expr of node.expressions) {
512
+ visit(expr);
513
+ }
514
+ }
515
+ return;
516
+ } else if (node.type === 'SpreadElement' || node.type === 'RestElement') {
517
+ // Visit the argument
518
+ if (node.argument) {
519
+ visit(node.argument);
520
+ }
521
+ return;
522
+ } else if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
523
+ // Visit the argument if present
524
+ if (node.argument) {
525
+ visit(node.argument);
526
+ }
527
+ return;
528
+ } else if (node.type === 'ChainExpression') {
529
+ // Visit the expression
530
+ if (node.expression) {
531
+ visit(node.expression);
532
+ }
533
+ return;
534
+ } else if (node.type === 'Super' || node.type === 'ThisExpression') {
535
+ // Leaf nodes, no children
536
+ return;
537
+ } else if (node.type === 'MetaProperty') {
538
+ // Visit meta and property (e.g., new.target, import.meta)
539
+ if (node.meta) {
540
+ visit(node.meta);
541
+ }
542
+ if (node.property) {
543
+ visit(node.property);
544
+ }
545
+ return;
546
+ } else if (node.type === 'EmptyStatement' || node.type === 'DebuggerStatement') {
547
+ // No children to visit
548
+ return;
549
+ } else if (node.type === 'LabeledStatement') {
550
+ // Visit label and statement
551
+ if (node.label) {
552
+ visit(node.label);
553
+ }
554
+ if (node.body) {
555
+ visit(node.body);
556
+ }
557
+ return;
558
+ } else if (node.type === 'BreakStatement' || node.type === 'ContinueStatement') {
559
+ // Visit label if present
560
+ if (node.label) {
561
+ visit(node.label);
562
+ }
563
+ return;
564
+ } else if (node.type === 'WithStatement') {
565
+ // Visit object and body
566
+ if (node.object) {
567
+ visit(node.object);
568
+ }
569
+ if (node.body) {
570
+ visit(node.body);
571
+ }
572
+ return;
573
+ } else if (node.type === 'JSXFragment') {
574
+ // Visit children in order
575
+ if (node.children) {
576
+ for (const child of node.children) {
577
+ visit(child);
578
+ }
579
+ }
580
+ return;
581
+ } else if (node.type === 'JSXClosingElement' || node.type === 'JSXClosingFragment' || node.type === 'JSXOpeningFragment') {
582
+ // These are handled by their parent nodes
583
+ return;
584
+ } else if (node.type === 'JSXMemberExpression') {
585
+ // Visit object and property (e.g., <Foo.Bar>)
586
+ if (node.object) {
587
+ visit(node.object);
588
+ }
589
+ if (node.property) {
590
+ visit(node.property);
591
+ }
592
+ return;
593
+ } else if (node.type === 'JSXNamespacedName') {
594
+ // Visit namespace and name (e.g., <svg:circle>)
595
+ if (node.namespace) {
596
+ visit(node.namespace);
597
+ }
598
+ if (node.name) {
599
+ visit(node.name);
600
+ }
601
+ return;
602
+ } else if (node.type === 'JSXEmptyExpression') {
603
+ // No children
604
+ return;
605
+ } else if (node.type === 'TemplateElement') {
606
+ // Leaf node, no children to visit
607
+ return;
608
+ } else if (node.type === 'PrivateIdentifier') {
609
+ // Leaf node
610
+ return;
611
+ } else if (node.type === 'PropertyDefinition') {
612
+ // Visit key and value
613
+ if (node.key) {
614
+ visit(node.key);
615
+ }
616
+ if (node.value) {
617
+ visit(node.value);
618
+ }
619
+ return;
620
+ } else if (node.type === 'StaticBlock') {
621
+ // Visit body
622
+ if (node.body) {
623
+ for (const statement of node.body) {
624
+ visit(statement);
625
+ }
626
+ }
627
+ return;
628
+ } else if (node.type === 'ImportExpression') {
629
+ // Visit source
630
+ if (node.source) {
631
+ visit(node.source);
632
+ }
633
+ return;
634
+ } else if (node.type === 'ParenthesizedExpression') {
635
+ // Visit the wrapped expression
636
+ if (node.expression) {
637
+ visit(node.expression);
638
+ }
639
+ return;
640
+ } else if (node.type === 'TSAsExpression' || node.type === 'TSSatisfiesExpression') {
641
+ // Type assertion: value as Type
642
+ if (node.expression) {
643
+ visit(node.expression);
644
+ }
645
+ // Skip typeAnnotation
646
+ return;
647
+ } else if (node.type === 'TSNonNullExpression') {
648
+ // Non-null assertion: value!
649
+ if (node.expression) {
650
+ visit(node.expression);
651
+ }
652
+ return;
653
+ } else if (node.type === 'TSTypeAssertion') {
654
+ // Type assertion: <Type>value
655
+ if (node.expression) {
656
+ visit(node.expression);
657
+ }
658
+ // Skip typeAnnotation
659
+ return;
660
+ } else if (node.type === 'TSTypeParameterInstantiation' || node.type === 'TSTypeParameterDeclaration') {
661
+ // Generic type parameters - visit to collect type variable names
662
+ if (node.params) {
663
+ for (const param of node.params) {
664
+ visit(param);
665
+ }
666
+ }
667
+ return;
668
+ } else if (node.type === 'TSTypeParameter') {
669
+ // Type parameter like T in <T>
670
+ if (node.name) {
671
+ visit(node.name);
672
+ }
673
+ if (node.constraint) {
674
+ visit(node.constraint);
675
+ }
676
+ if (node.default) {
677
+ visit(node.default);
678
+ }
679
+ return;
680
+ } else if (node.type === 'TSTypeAnnotation') {
681
+ // Type annotation - visit the type
682
+ if (node.typeAnnotation) {
683
+ visit(node.typeAnnotation);
684
+ }
685
+ return;
686
+ } else if (node.type === 'TSTypeReference') {
687
+ // Type reference like "string" or "Array<T>"
688
+ if (node.typeName) {
689
+ visit(node.typeName);
690
+ }
691
+ if (node.typeParameters) {
692
+ visit(node.typeParameters);
693
+ }
694
+ return;
695
+ } else if (node.type === 'TSQualifiedName') {
696
+ // Qualified name (e.g., Foo.Bar in types)
697
+ if (node.left) {
698
+ visit(node.left);
699
+ }
700
+ if (node.right) {
701
+ visit(node.right);
702
+ }
703
+ return;
704
+ } else if (node.type === 'TSArrayType') {
705
+ // Array type like T[]
706
+ if (node.elementType) {
707
+ visit(node.elementType);
708
+ }
709
+ return;
710
+ } else if (node.type === 'TSTupleType') {
711
+ // Tuple type like [string, number]
712
+ if (node.elementTypes) {
713
+ for (const type of node.elementTypes) {
714
+ visit(type);
715
+ }
716
+ }
717
+ return;
718
+ } else if (node.type === 'TSUnionType' || node.type === 'TSIntersectionType') {
719
+ // Union (A | B) or Intersection (A & B) types
720
+ if (node.types) {
721
+ for (const type of node.types) {
722
+ visit(type);
723
+ }
724
+ }
725
+ return;
726
+ } else if (node.type === 'TSFunctionType' || node.type === 'TSConstructorType') {
727
+ // Function or constructor type
728
+ if (node.typeParameters) {
729
+ visit(node.typeParameters);
730
+ }
731
+ if (node.parameters) {
732
+ for (const param of node.parameters) {
733
+ visit(param);
734
+ }
735
+ }
736
+ if (node.typeAnnotation) {
737
+ visit(node.typeAnnotation);
738
+ }
739
+ return;
740
+ } else if (node.type === 'TSTypeLiteral') {
741
+ // Object type literal { foo: string }
742
+ if (node.members) {
743
+ for (const member of node.members) {
744
+ visit(member);
745
+ }
746
+ }
747
+ return;
748
+ } else if (node.type === 'TSPropertySignature') {
749
+ // Property signature in type
750
+ if (node.key) {
751
+ visit(node.key);
752
+ }
753
+ if (node.typeAnnotation) {
754
+ visit(node.typeAnnotation);
755
+ }
756
+ return;
757
+ } else if (node.type === 'TSMethodSignature') {
758
+ // Method signature in type
759
+ if (node.key) {
760
+ visit(node.key);
761
+ }
762
+ if (node.typeParameters) {
763
+ visit(node.typeParameters);
764
+ }
765
+ if (node.parameters) {
766
+ for (const param of node.parameters) {
767
+ visit(param);
768
+ }
769
+ }
770
+ if (node.typeAnnotation) {
771
+ visit(node.typeAnnotation);
772
+ }
773
+ return;
774
+ } else if (node.type === 'TSIndexSignature') {
775
+ // Index signature [key: string]: Type
776
+ if (node.parameters) {
777
+ for (const param of node.parameters) {
778
+ visit(param);
779
+ }
780
+ }
781
+ if (node.typeAnnotation) {
782
+ visit(node.typeAnnotation);
783
+ }
784
+ return;
785
+ } else if (node.type === 'TSCallSignatureDeclaration' || node.type === 'TSConstructSignatureDeclaration') {
786
+ // Call or construct signature
787
+ if (node.typeParameters) {
788
+ visit(node.typeParameters);
789
+ }
790
+ if (node.parameters) {
791
+ for (const param of node.parameters) {
792
+ visit(param);
793
+ }
794
+ }
795
+ if (node.typeAnnotation) {
796
+ visit(node.typeAnnotation);
797
+ }
798
+ return;
799
+ } else if (node.type === 'TSConditionalType') {
800
+ // Conditional type: T extends U ? X : Y
801
+ if (node.checkType) {
802
+ visit(node.checkType);
803
+ }
804
+ if (node.extendsType) {
805
+ visit(node.extendsType);
806
+ }
807
+ if (node.trueType) {
808
+ visit(node.trueType);
809
+ }
810
+ if (node.falseType) {
811
+ visit(node.falseType);
812
+ }
813
+ return;
814
+ } else if (node.type === 'TSInferType') {
815
+ // Infer type: infer T
816
+ if (node.typeParameter) {
817
+ visit(node.typeParameter);
818
+ }
819
+ return;
820
+ } else if (node.type === 'TSParenthesizedType') {
821
+ // Parenthesized type: (T)
822
+ if (node.typeAnnotation) {
823
+ visit(node.typeAnnotation);
824
+ }
825
+ return;
826
+ } else if (node.type === 'TSTypeOperator') {
827
+ // Type operator: keyof T, readonly T
828
+ if (node.typeAnnotation) {
829
+ visit(node.typeAnnotation);
830
+ }
831
+ return;
832
+ } else if (node.type === 'TSIndexedAccessType') {
833
+ // Indexed access: T[K]
834
+ if (node.objectType) {
835
+ visit(node.objectType);
836
+ }
837
+ if (node.indexType) {
838
+ visit(node.indexType);
839
+ }
840
+ return;
841
+ } else if (node.type === 'TSMappedType') {
842
+ // Mapped type: { [K in keyof T]: ... }
843
+ if (node.typeParameter) {
844
+ visit(node.typeParameter);
845
+ }
846
+ if (node.typeAnnotation) {
847
+ visit(node.typeAnnotation);
848
+ }
849
+ return;
850
+ } else if (node.type === 'TSLiteralType') {
851
+ // Literal type: "foo" | 123 | true
852
+ if (node.literal) {
853
+ visit(node.literal);
854
+ }
855
+ return;
856
+ } else if (node.type === 'TSExpressionWithTypeArguments') {
857
+ // Expression with type arguments: Foo<Bar>
858
+ if (node.expression) {
859
+ visit(node.expression);
860
+ }
861
+ if (node.typeParameters) {
862
+ visit(node.typeParameters);
863
+ }
864
+ return;
865
+ } else if (node.type === 'TSImportType') {
866
+ // Import type: import("module").Type
867
+ if (node.argument) {
868
+ visit(node.argument);
869
+ }
870
+ if (node.qualifier) {
871
+ visit(node.qualifier);
872
+ }
873
+ if (node.typeParameters) {
874
+ visit(node.typeParameters);
875
+ }
876
+ return;
877
+ } else if (node.type === 'TSTypeQuery') {
878
+ // Type query: typeof x
879
+ if (node.exprName) {
880
+ visit(node.exprName);
881
+ }
882
+ return;
883
+ } else if (node.type === 'TSInterfaceDeclaration') {
884
+ // Interface declaration
885
+ if (node.id) {
886
+ visit(node.id);
887
+ }
888
+ if (node.typeParameters) {
889
+ visit(node.typeParameters);
890
+ }
891
+ if (node.extends) {
892
+ for (const ext of node.extends) {
893
+ visit(ext);
894
+ }
895
+ }
896
+ if (node.body) {
897
+ visit(node.body);
898
+ }
899
+ return;
900
+ } else if (node.type === 'TSInterfaceBody') {
901
+ // Interface body
902
+ if (node.body) {
903
+ for (const member of node.body) {
904
+ visit(member);
905
+ }
906
+ }
907
+ return;
908
+ } else if (node.type === 'TSTypeAliasDeclaration') {
909
+ // Type alias
910
+ if (node.id) {
911
+ visit(node.id);
912
+ }
913
+ if (node.typeParameters) {
914
+ visit(node.typeParameters);
915
+ }
916
+ if (node.typeAnnotation) {
917
+ visit(node.typeAnnotation);
918
+ }
919
+ return;
920
+ } else if (node.type === 'TSEnumDeclaration') {
921
+ // Visit id and members
922
+ if (node.id) {
923
+ visit(node.id);
924
+ }
925
+ if (node.members) {
926
+ for (const member of node.members) {
927
+ visit(member);
928
+ }
929
+ }
930
+ return;
931
+ } else if (node.type === 'TSEnumMember') {
932
+ // Visit id and initializer
933
+ if (node.id) {
934
+ visit(node.id);
935
+ }
936
+ if (node.initializer) {
937
+ visit(node.initializer);
938
+ }
939
+ return;
940
+ } else if (node.type === 'TSModuleDeclaration') {
941
+ // Namespace/module declaration
942
+ if (node.id) {
943
+ visit(node.id);
944
+ }
945
+ if (node.body) {
946
+ visit(node.body);
947
+ }
948
+ return;
949
+ } else if (node.type === 'TSModuleBlock') {
950
+ // Module body
951
+ if (node.body) {
952
+ for (const statement of node.body) {
953
+ visit(statement);
954
+ }
955
+ }
956
+ return;
957
+ } else if (node.type === 'TSNamedTupleMember') {
958
+ // Named tuple member: [name: Type]
959
+ if (node.label) {
960
+ visit(node.label);
961
+ }
962
+ if (node.elementType) {
963
+ visit(node.elementType);
964
+ }
965
+ return;
966
+ } else if (node.type === 'TSRestType') {
967
+ // Rest type: ...T[]
968
+ if (node.typeAnnotation) {
969
+ visit(node.typeAnnotation);
970
+ }
971
+ return;
972
+ } else if (node.type === 'TSOptionalType') {
973
+ // Optional type: T?
974
+ if (node.typeAnnotation) {
975
+ visit(node.typeAnnotation);
976
+ }
977
+ return;
978
+ } else if (node.type === 'TSAnyKeyword' || node.type === 'TSUnknownKeyword' || node.type === 'TSNumberKeyword' || node.type === 'TSObjectKeyword' || node.type === 'TSBooleanKeyword' || node.type === 'TSBigIntKeyword' || node.type === 'TSStringKeyword' || node.type === 'TSSymbolKeyword' || node.type === 'TSVoidKeyword' || node.type === 'TSUndefinedKeyword' || node.type === 'TSNullKeyword' || node.type === 'TSNeverKeyword' || node.type === 'TSThisType' || node.type === 'TSIntrinsicKeyword') {
979
+ // Primitive type keywords - leaf nodes, no children
980
+ return;
462
981
  }
463
982
 
464
- next();
983
+ throw new Error(`Unhandled AST node type in mapping walker: ${node.type}`);
465
984
  }
466
985
  });
467
986
 
@@ -16,6 +16,7 @@ import {
16
16
  import is_reference from 'is-reference';
17
17
  import { escape } from '../../../../utils/escaping.js';
18
18
  import { is_event_attribute } from '../../../../utils/events.js';
19
+ import { render_stylesheets } from '../stylesheet.js';
19
20
 
20
21
  function add_ripple_internal_import(context) {
21
22
  if (!context.state.to_ts) {
@@ -93,17 +94,33 @@ const visitors = {
93
94
 
94
95
  if (node.css !== null && node.css) {
95
96
  context.state.stylesheets.push(node.css);
97
+ // Register CSS hash during rendering
98
+ body_statements.unshift(
99
+ b.stmt(
100
+ b.call(
101
+ b.member(b.id('__output'), b.id('register_css')),
102
+ b.literal(node.css.hash),
103
+ ),
104
+ ),
105
+ );
96
106
  }
97
107
 
98
- return b.function(
108
+ let component_fn = b.function(
99
109
  node.id,
100
110
  node.params.length > 0 ? [b.id('__output'), node.params[0]] : [b.id('__output')],
101
111
  b.block([
102
112
  ...(metadata.await
103
- ? [b.stmt(b.call('_$_.async', b.thunk(b.block(body_statements), true)))]
113
+ ? [b.return(b.call('_$_.async', b.thunk(b.block(body_statements), true)))]
104
114
  : body_statements),
105
115
  ]),
106
116
  );
117
+
118
+ // Mark function as async if needed
119
+ if (metadata.await) {
120
+ component_fn = b.async(component_fn);
121
+ }
122
+
123
+ return component_fn;
107
124
  },
108
125
 
109
126
  CallExpression(node, context) {
@@ -127,6 +144,20 @@ const visitors = {
127
144
  return context.next();
128
145
  },
129
146
 
147
+ TSTypeAliasDeclaration(_, context) {
148
+ if (!context.state.to_ts) {
149
+ return b.empty;
150
+ }
151
+ context.next();
152
+ },
153
+
154
+ TSInterfaceDeclaration(_, context) {
155
+ if (!context.state.to_ts) {
156
+ return b.empty;
157
+ }
158
+ context.next();
159
+ },
160
+
130
161
  ExportNamedDeclaration(node, context) {
131
162
  if (!context.state.to_ts && node.exportKind === 'type') {
132
163
  return b.empty;
@@ -164,13 +195,10 @@ const visitors = {
164
195
  let class_attribute = null;
165
196
 
166
197
  const handle_static_attr = (name, value) => {
167
- const attr_value = b.literal(
168
- ` ${name}${
169
- is_boolean_attribute(name) && value === true
170
- ? ''
171
- : `="${value === true ? '' : escape_html(value, true)}"`
172
- }`,
173
- );
198
+ const attr_str = ` ${name}${is_boolean_attribute(name) && value === true
199
+ ? ''
200
+ : `="${value === true ? '' : escape_html(value, true)}"`
201
+ }`;
174
202
 
175
203
  if (is_spreading) {
176
204
  // For spread attributes, store just the actual value, not the full attribute string
@@ -181,7 +209,7 @@ const visitors = {
181
209
  spread_attributes.push(b.prop('init', b.literal(name), actual_value));
182
210
  } else {
183
211
  state.init.push(
184
- b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(String(attr_value)))),
212
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(attr_str))),
185
213
  );
186
214
  }
187
215
  };
@@ -241,7 +269,8 @@ const visitors = {
241
269
  let expression = visit(class_attribute.value, { ...state, metadata });
242
270
 
243
271
  if (node.metadata.scoped && state.component.css) {
244
- expression = b.binary('+', b.literal(state.component.css.hash + ' '), expression);
272
+ // Pass array to clsx so it can handle objects properly
273
+ expression = b.array([expression, b.literal(state.component.css.hash)]);
245
274
  }
246
275
 
247
276
  state.init.push(
@@ -256,7 +285,7 @@ const visitors = {
256
285
  } else if (node.metadata.scoped && state.component.css) {
257
286
  const value = state.component.css.hash;
258
287
 
259
- // TOOO
288
+ handle_static_attr('class', value);
260
289
  }
261
290
 
262
291
  if (spread_attributes !== null && spread_attributes.length > 0) {
@@ -337,11 +366,33 @@ const visitors = {
337
366
  }
338
367
  }
339
368
 
340
- state.init.push(b.stmt(b.call(visit(node.id, state), b.id('__output'), b.object(props))));
341
- }
342
- },
369
+ // For SSR, determine if we should await based on component metadata
370
+ const component_call = b.call(visit(node.id, state), b.id('__output'), b.object(props));
371
+
372
+ // Check if this is a locally defined component and if it's async
373
+ const component_name = node.id.type === 'Identifier' ? node.id.name : null;
374
+ const local_metadata = component_name
375
+ ? state.component_metadata.find((m) => m.id === component_name)
376
+ : null;
343
377
 
344
- ForOfStatement(node, context) {
378
+ if (local_metadata) {
379
+ // Component is defined locally - we know if it's async or not
380
+ if (local_metadata.async) {
381
+ state.init.push(b.stmt(b.await(component_call)));
382
+ } else {
383
+ state.init.push(b.stmt(component_call));
384
+ }
385
+ } else {
386
+ // Component is imported or dynamic - check .async property at runtime
387
+ const conditional_await = b.conditional(
388
+ b.member(visit(node.id, state), b.id('async')),
389
+ b.await(component_call),
390
+ component_call
391
+ );
392
+ state.init.push(b.stmt(conditional_await));
393
+ }
394
+ }
395
+ }, ForOfStatement(node, context) {
345
396
  if (!is_inside_component(context)) {
346
397
  context.next();
347
398
  return;
@@ -392,6 +443,131 @@ const visitors = {
392
443
  }
393
444
  },
394
445
 
446
+ ImportDeclaration(node, context) {
447
+ if (!context.state.to_ts && node.importKind === 'type') {
448
+ return b.empty;
449
+ }
450
+
451
+ return {
452
+ ...node,
453
+ specifiers: node.specifiers
454
+ .filter((spec) => spec.importKind !== 'type')
455
+ .map((spec) => context.visit(spec)),
456
+ };
457
+ },
458
+
459
+ TryStatement(node, context) {
460
+ if (!is_inside_component(context)) {
461
+ return context.next();
462
+ }
463
+
464
+ // If there's a pending block, this is an async operation
465
+ const has_pending = node.pending !== null;
466
+ if (has_pending && context.state.metadata?.await === false) {
467
+ context.state.metadata.await = true;
468
+ }
469
+
470
+ const metadata = { await: false };
471
+ const body = transform_body(node.block.body, {
472
+ ...context,
473
+ state: { ...context.state, metadata },
474
+ });
475
+
476
+ // Check if the try block itself contains async operations
477
+ const is_async = metadata.await || has_pending;
478
+
479
+ if (is_async) {
480
+ if (context.state.metadata?.await === false) {
481
+ context.state.metadata.await = true;
482
+ }
483
+
484
+ // For SSR with pending block: render the resolved content wrapped in async
485
+ // In a streaming SSR implementation, we'd render pending first, then stream resolved
486
+ const try_statements = node.handler !== null
487
+ ? [
488
+ b.try(
489
+ b.block(body),
490
+ b.catch_clause(
491
+ node.handler.param || b.id('error'),
492
+ b.block(
493
+ transform_body(node.handler.body.body, {
494
+ ...context,
495
+ state: { ...context.state, scope: context.state.scopes.get(node.handler.body) },
496
+ }),
497
+ ),
498
+ ),
499
+ ),
500
+ ]
501
+ : body;
502
+
503
+ context.state.init.push(
504
+ b.stmt(b.await(b.call('_$_.async', b.thunk(b.block(try_statements), true)))),
505
+ );
506
+ } else {
507
+ // No async, just regular try/catch
508
+ if (node.handler !== null) {
509
+ const handler_body = transform_body(node.handler.body.body, {
510
+ ...context,
511
+ state: { ...context.state, scope: context.state.scopes.get(node.handler.body) },
512
+ });
513
+
514
+ context.state.init.push(
515
+ b.try(
516
+ b.block(body),
517
+ b.catch_clause(
518
+ node.handler.param || b.id('error'),
519
+ b.block(handler_body),
520
+ ),
521
+ ),
522
+ );
523
+ } else {
524
+ context.state.init.push(...body);
525
+ }
526
+ }
527
+ },
528
+
529
+ AwaitExpression(node, context) {
530
+ if (context.state.to_ts) {
531
+ return context.next();
532
+ }
533
+
534
+ if (context.state.metadata?.await === false) {
535
+ context.state.metadata.await = true;
536
+ }
537
+
538
+ return b.await(context.visit(node.argument));
539
+ },
540
+
541
+ TrackedObjectExpression(node, context) {
542
+ // For SSR, we just evaluate the object as-is since there's no reactivity
543
+ return b.object(node.properties.map((prop) => context.visit(prop)));
544
+ },
545
+
546
+ TrackedArrayExpression(node, context) {
547
+ // For SSR, we just evaluate the array as-is since there's no reactivity
548
+ return b.array(node.elements.map((el) => context.visit(el)));
549
+ },
550
+
551
+ MemberExpression(node, context) {
552
+ const parent = context.path.at(-1);
553
+
554
+ if (node.tracked || (node.property.type === 'Identifier' && node.property.tracked)) {
555
+ add_ripple_internal_import(context);
556
+
557
+ return b.call(
558
+ '_$_.get',
559
+ b.member(
560
+ context.visit(node.object),
561
+ node.computed ? context.visit(node.property) : node.property,
562
+ node.computed,
563
+ node.optional,
564
+ ),
565
+ );
566
+ }
567
+
568
+ return context.next();
569
+ },
570
+
395
571
  Text(node, { visit, state }) {
396
572
  const metadata = { await: false };
397
573
  const expression = visit(node.expression, { ...state, metadata });
@@ -408,21 +584,66 @@ const visitors = {
408
584
  );
409
585
  }
410
586
  },
587
+
588
+ Html(node, { visit, state }) {
589
+ const metadata = { await: false };
590
+ const expression = visit(node.expression, { ...state, metadata });
591
+
592
+ // For Html nodes, we render the content as-is without escaping
593
+ if (expression.type === 'Literal') {
594
+ state.init.push(
595
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(expression.value))),
596
+ );
597
+ } else {
598
+ // If it's dynamic, we need to evaluate it and push it directly (not escaped)
599
+ state.init.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), expression)));
600
+ }
601
+ },
411
602
  };
412
603
 
413
604
  export function transform_server(filename, source, analysis) {
605
+ // Use component metadata collected during the analyze phase
606
+ const component_metadata = analysis.component_metadata || [];
607
+
414
608
  const state = {
415
609
  imports: new Set(),
416
610
  init: null,
417
611
  scope: analysis.scope,
418
612
  scopes: analysis.scopes,
419
613
  stylesheets: [],
614
+ component_metadata,
420
615
  };
421
616
 
422
617
  const program = /** @type {ESTree.Program} */ (
423
618
  walk(analysis.ast, { ...state, namespace: 'html' }, visitors)
424
619
  );
425
620
 
621
+ const css = render_stylesheets(state.stylesheets);
622
+
623
+ // Add CSS registration if there are stylesheets
624
+ if (state.stylesheets.length > 0 && css) {
625
+ // Register each stylesheet's CSS
626
+ for (const stylesheet of state.stylesheets) {
627
+ const css_for_component = render_stylesheets([stylesheet]);
628
+ program.body.push(
629
+ b.stmt(
630
+ b.call('_$_.register_css', b.literal(stylesheet.hash), b.literal(css_for_component)),
631
+ ),
632
+ );
633
+ }
634
+ }
635
+
636
+ // Add async property to component functions
637
+ for (const metadata of state.component_metadata) {
638
+ if (metadata.async) {
639
+ program.body.push(
640
+ b.stmt(
641
+ b.assignment('=', b.member(b.id(metadata.id), b.id('async')), b.true),
642
+ ),
643
+ );
644
+ }
645
+ }
646
+
426
647
  for (const import_node of state.imports) {
427
648
  program.body.unshift(b.stmt(b.id(import_node)));
428
649
  }
@@ -432,9 +653,6 @@ export function transform_server(filename, source, analysis) {
432
653
  sourceMapSource: path.basename(filename),
433
654
  });
434
655
 
435
- // TODO: extract css
436
- const css = '';
437
-
438
656
  return {
439
657
  ast: program,
440
658
  js,
@@ -5,6 +5,7 @@ import { handle_root_events } from './internal/client/events.js';
5
5
  import { init_operations } from './internal/client/operations.js';
6
6
  import { active_block, tracked, derived } from './internal/client/runtime.js';
7
7
  import { create_anchor } from './internal/client/utils.js';
8
+ import { remove_ssr_css } from './internal/client/css.js';
8
9
 
9
10
  // Re-export JSX runtime functions for jsxImportSource: "ripple"
10
11
  export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
@@ -16,6 +17,7 @@ export { jsx, jsxs, Fragment } from '../jsx-runtime.js';
16
17
  */
17
18
  export function mount(component, options) {
18
19
  init_operations();
20
+ remove_ssr_css();
19
21
 
20
22
  const props = options.props || {};
21
23
  const target = options.target;
@@ -0,0 +1,68 @@
1
+ export function remove_ssr_css() {
2
+ if (!document || typeof requestAnimationFrame !== "function") {
3
+ return;
4
+ }
5
+
6
+ remove_styles();
7
+ }
8
+
9
+ function remove_styles() {
10
+ if (import.meta.env.DEV) {
11
+ const styles = document.querySelector('style[data-vite-dev-id]');
12
+ if (styles) {
13
+ remove();
14
+ } else {
15
+ requestAnimationFrame(remove_styles);
16
+ }
17
+ } else if (import.meta.env.PROD) {
18
+ remove_when_css_loaded(() => requestAnimationFrame(remove));
19
+ }
20
+ }
21
+
22
+ function remove() {
23
+ document.querySelectorAll("style[data-ripple-ssr]").forEach((el) => el.remove());
24
+ }
25
+
26
+ /**
27
+ * @param {function} callback
28
+ * @returns {void}
29
+ */
30
+ function remove_when_css_loaded(callback) {
31
+ /** @type {HTMLLinkElement[]} */
32
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
33
+ let remaining = links.length;
34
+
35
+ if (remaining === 0) {
36
+ callback();
37
+ return;
38
+ }
39
+
40
+ const done = () => {
41
+ remaining--;
42
+ if (remaining === 0) {
43
+ // clean up all listeners
44
+ links.forEach((link) => {
45
+ link.removeEventListener('load', onLoad);
46
+ link.removeEventListener('error', onError);
47
+ });
48
+ callback();
49
+ }
50
+ };
51
+
52
+ function onLoad() {
53
+ done();
54
+ }
55
+ function onError() {
56
+ done();
57
+ }
58
+
59
+ links.forEach((link) => {
60
+ if (link.sheet) {
61
+ // already loaded (possibly cached)
62
+ done();
63
+ } else {
64
+ link.addEventListener('load', onLoad);
65
+ link.addEventListener('error', onError);
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Global CSS registry for SSR
3
+ * Maps CSS hashes to their content
4
+ * This persists across requests for performance (CSS is immutable per hash)
5
+ * @type {Map<string, string>}
6
+ */
7
+ const css_registry = new Map();
8
+
9
+ /**
10
+ * Register a component's CSS
11
+ * Only sets if the hash doesn't already exist (CSS is immutable per hash)
12
+ * @param {string} hash - The CSS hash
13
+ * @param {string} content - The CSS content
14
+ */
15
+ export function register_component_css(hash, content) {
16
+ if (!css_registry.has(hash)) {
17
+ css_registry.set(hash, content);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Get CSS content for a set of hashes
23
+ * @param {Set<string>} hashes
24
+ * @returns {string}
25
+ */
26
+ export function get_css_for_hashes(hashes) {
27
+ const css_parts = [];
28
+ for (const hash of hashes) {
29
+ const content = css_registry.get(hash);
30
+ if (content) {
31
+ css_parts.push(content);
32
+ }
33
+ }
34
+ return css_parts.join('\n');
35
+ }
@@ -1,14 +1,12 @@
1
1
  /** @import { Component, Derived } from '#server' */
2
-
3
- import { DERIVED, UNINITIALIZED } from '../client/constants';
4
- import { is_tracked_object } from '../client/utils';
5
-
2
+ import { DERIVED, UNINITIALIZED } from '../client/constants.js';
3
+ import { is_tracked_object } from '../client/utils.js';
6
4
  import { escape } from '../../../utils/escaping.js';
7
5
  import { is_boolean_attribute } from '../../../compiler/utils';
8
-
9
6
  import { clsx } from 'clsx';
10
7
 
11
8
  export { escape };
9
+ export { register_component_css as register_css } from './css-registry.js';
12
10
 
13
11
  /** @type {Component | null} */
14
12
  export let active_component = null;
@@ -29,6 +27,8 @@ const replacements = {
29
27
  class Output {
30
28
  head = '';
31
29
  body = '';
30
+ /** @type {Set<string>} */
31
+ css = new Set();
32
32
  /** @type {Output | null} */
33
33
  #parent = null;
34
34
 
@@ -46,25 +46,32 @@ class Output {
46
46
  push(str) {
47
47
  this.body += str;
48
48
  }
49
+
50
+ /**
51
+ * @param {string} hash
52
+ * @returns {void}
53
+ */
54
+ register_css(hash) {
55
+ this.css.add(hash);
56
+ }
49
57
  }
50
58
 
51
59
  /**
52
60
  * @param {((output: Output, props: Record<string, any>) => void | Promise<void>) & { async?: boolean }} component
53
- * @returns {Promise<{head: string, body: string}>}
61
+ * @returns {Promise<{head: string, body: string, css: Set<string>}>}
54
62
  */
55
63
  export async function render(component) {
56
64
  const output = new Output(null);
57
65
 
58
- // TODO add expando "async" property to component functions during SSR
59
66
  if (component.async) {
60
67
  await component(output, {});
61
68
  } else {
62
69
  component(output, {});
63
70
  }
64
71
 
65
- const { head, body } = output;
72
+ const { head, body, css } = output;
66
73
 
67
- return { head, body };
74
+ return { head, body, css };
68
75
  }
69
76
 
70
77
  /**
@@ -91,7 +98,15 @@ export function pop_component() {
91
98
  * @returns {Promise<void>}
92
99
  */
93
100
  export async function async(fn) {
94
- // TODO
101
+ await fn();
102
+ }
103
+
104
+ /**
105
+ * @returns {boolean}
106
+ */
107
+ export function aborted() {
108
+ // For SSR, we don't abort rendering
109
+ return false;
95
110
  }
96
111
 
97
112
  /**
@@ -118,7 +133,7 @@ export function get(tracked) {
118
133
  return tracked;
119
134
  }
120
135
 
121
- return (tracked.f & DERIVED) !== 0 ? get_derived(/** @type {Derived} */ (tracked)) : tracked.v;
136
+ return (tracked.f & DERIVED) !== 0 ? get_derived(/** @type {Derived} */(tracked)) : tracked.v;
122
137
  }
123
138
 
124
139
  /**
@@ -153,13 +168,13 @@ export function spread_attrs(attrs, css_hash) {
153
168
 
154
169
  if (typeof value === 'function') continue;
155
170
 
156
- if (is_tracked_object(value)) {
157
- value = get(value);
158
- }
171
+ if (is_tracked_object(value)) {
172
+ value = get(value);
173
+ }
159
174
 
160
- if (name === 'class' && css_hash) {
161
- value = value == null ? css_hash : [value, css_hash];
162
- }
175
+ if (name === 'class' && css_hash) {
176
+ value = value == null ? css_hash : [value, css_hash];
177
+ }
163
178
 
164
179
  attr_str += attr(name, value, is_boolean_attribute(name));
165
180
  }
@@ -1 +1,2 @@
1
- export { render } from '../runtime/internal/server/index.js';
1
+ export { render } from '../runtime/internal/server/index.js';
2
+ export { get_css_for_hashes } from '../runtime/internal/server/css-registry.js';