ripple 0.2.208 → 0.2.210

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 (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +2 -1
  3. package/package.json +2 -6
  4. package/shims/rollup-estree-types.d.ts +1 -1
  5. package/src/compiler/index.d.ts +1 -0
  6. package/src/compiler/index.js +7 -1
  7. package/src/compiler/phases/1-parse/index.js +15 -6
  8. package/src/compiler/phases/2-analyze/css-analyze.js +100 -104
  9. package/src/compiler/phases/2-analyze/index.js +215 -2
  10. package/src/compiler/phases/3-transform/client/index.js +388 -50
  11. package/src/compiler/phases/3-transform/segments.js +123 -39
  12. package/src/compiler/phases/3-transform/server/index.js +266 -13
  13. package/src/compiler/types/index.d.ts +16 -3
  14. package/src/compiler/utils.js +1 -15
  15. package/src/constants.js +0 -2
  16. package/src/helpers.d.ts +4 -0
  17. package/src/html-tree-validation.js +211 -0
  18. package/src/jsx-runtime.d.ts +260 -259
  19. package/src/jsx-runtime.js +12 -12
  20. package/src/runtime/array.js +17 -17
  21. package/src/runtime/create-subscriber.js +1 -1
  22. package/src/runtime/index-client.js +1 -5
  23. package/src/runtime/index-server.js +15 -0
  24. package/src/runtime/internal/client/compat.js +3 -3
  25. package/src/runtime/internal/client/composite.js +6 -1
  26. package/src/runtime/internal/client/head.js +50 -4
  27. package/src/runtime/internal/client/html.js +73 -12
  28. package/src/runtime/internal/client/hydration.js +12 -0
  29. package/src/runtime/internal/client/index.js +1 -1
  30. package/src/runtime/internal/client/portal.js +54 -29
  31. package/src/runtime/internal/client/rpc.js +3 -1
  32. package/src/runtime/internal/client/switch.js +5 -0
  33. package/src/runtime/internal/client/template.js +117 -11
  34. package/src/runtime/internal/client/try.js +1 -0
  35. package/src/runtime/internal/server/index.js +113 -1
  36. package/src/runtime/internal/server/rpc.js +4 -4
  37. package/src/runtime/map.js +2 -2
  38. package/src/runtime/object.js +6 -6
  39. package/src/runtime/proxy.js +12 -11
  40. package/src/runtime/reactive-value.js +9 -1
  41. package/src/runtime/set.js +12 -7
  42. package/src/runtime/url-search-params.js +0 -1
  43. package/src/server/index.js +4 -0
  44. package/src/utils/hashing.js +15 -0
  45. package/src/utils/normalize_css_property_name.js +1 -1
  46. package/tests/client/array/array.mutations.test.ripple +8 -8
  47. package/tests/client/basic/basic.errors.test.ripple +28 -0
  48. package/tests/client/basic/basic.events.test.ripple +6 -3
  49. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  50. package/tests/client/compiler/compiler.regex.test.ripple +10 -8
  51. package/tests/client/composite/composite.generics.test.ripple +5 -2
  52. package/tests/client/dynamic-elements.test.ripple +30 -1
  53. package/tests/client/function-overload-import.ripple +6 -7
  54. package/tests/client/html.test.ripple +0 -1
  55. package/tests/client/object.test.ripple +2 -2
  56. package/tests/client/portal.test.ripple +3 -3
  57. package/tests/client/return.test.ripple +2500 -0
  58. package/tests/client/try.test.ripple +69 -0
  59. package/tests/client/typescript-generics.test.ripple +1 -1
  60. package/tests/client/url/url.derived.test.ripple +1 -1
  61. package/tests/client/url/url.parsing.test.ripple +3 -3
  62. package/tests/client/url/url.partial-removal.test.ripple +7 -7
  63. package/tests/client/url/url.reactivity.test.ripple +15 -15
  64. package/tests/client/url/url.serialization.test.ripple +2 -2
  65. package/tests/hydration/basic.test.js +23 -0
  66. package/tests/hydration/build-components.js +10 -4
  67. package/tests/hydration/compiled/client/basic.js +165 -3
  68. package/tests/hydration/compiled/client/for.js +1140 -23
  69. package/tests/hydration/compiled/client/head.js +234 -0
  70. package/tests/hydration/compiled/client/html.js +135 -0
  71. package/tests/hydration/compiled/client/portal.js +172 -0
  72. package/tests/hydration/compiled/client/reactivity.js +3 -1
  73. package/tests/hydration/compiled/client/return.js +1976 -0
  74. package/tests/hydration/compiled/client/switch.js +162 -0
  75. package/tests/hydration/compiled/server/basic.js +249 -0
  76. package/tests/hydration/compiled/server/events.js +1 -1
  77. package/tests/hydration/compiled/server/for.js +891 -1
  78. package/tests/hydration/compiled/server/head.js +291 -0
  79. package/tests/hydration/compiled/server/html.js +133 -0
  80. package/tests/hydration/compiled/server/if.js +1 -1
  81. package/tests/hydration/compiled/server/portal.js +250 -0
  82. package/tests/hydration/compiled/server/reactivity.js +1 -1
  83. package/tests/hydration/compiled/server/return.js +1969 -0
  84. package/tests/hydration/compiled/server/switch.js +130 -0
  85. package/tests/hydration/components/basic.ripple +55 -0
  86. package/tests/hydration/components/for.ripple +403 -0
  87. package/tests/hydration/components/head.ripple +111 -0
  88. package/tests/hydration/components/html.ripple +38 -0
  89. package/tests/hydration/components/portal.ripple +49 -0
  90. package/tests/hydration/components/return.ripple +564 -0
  91. package/tests/hydration/components/switch.ripple +51 -0
  92. package/tests/hydration/for.test.js +363 -0
  93. package/tests/hydration/head.test.js +105 -0
  94. package/tests/hydration/html.test.js +46 -0
  95. package/tests/hydration/portal.test.js +71 -0
  96. package/tests/hydration/return.test.js +544 -0
  97. package/tests/hydration/switch.test.js +42 -0
  98. package/tests/server/basic.attributes.test.ripple +1 -1
  99. package/tests/server/compiler.test.ripple +22 -0
  100. package/tests/server/composite.test.ripple +5 -2
  101. package/tests/server/html-nesting-validation.test.ripple +237 -0
  102. package/tests/server/return.test.ripple +1379 -0
  103. package/tests/setup-hydration.js +6 -1
  104. package/tests/utils/escaping.test.js +3 -1
  105. package/tests/utils/normalize_css_property_name.test.js +0 -1
  106. package/tests/utils/patterns.test.js +6 -2
  107. package/tests/utils/sanitize_template_string.test.js +3 -2
  108. package/types/server.d.ts +16 -0
@@ -675,22 +675,18 @@ export function convert_source_map_to_mappings(
675
675
 
676
676
  if (opening.loc) {
677
677
  // Add tokens for '<' and '>' brackets to ensure auto-close feature works
678
- if (opening.loc) {
679
- // Add '<' bracket
680
- tokens.push({
681
- source: '<',
682
- generated: '<',
683
- loc: {
684
- start: { line: opening.loc.start.line, column: opening.loc.start.column },
685
- end: { line: opening.loc.start.line, column: opening.loc.start.column + 1 },
686
- },
687
- metadata: {},
688
- mappingData: mapping_data_verify_only,
689
- });
690
- }
678
+ tokens.push({
679
+ source: '<',
680
+ generated: '<',
681
+ loc: {
682
+ start: { line: opening.loc.start.line, column: opening.loc.start.column },
683
+ end: { line: opening.loc.start.line, column: opening.loc.start.column + 1 },
684
+ },
685
+ metadata: {},
686
+ mappingData: mapping_data_verify_only,
687
+ });
691
688
 
692
689
  if (!opening.selfClosing) {
693
- // Add '>' bracket
694
690
  tokens.push({
695
691
  source: '>',
696
692
  generated: '>',
@@ -715,17 +711,28 @@ export function convert_source_map_to_mappings(
715
711
  }
716
712
  }
717
713
 
718
- if (closing) {
719
- // Add the whole closing tag
720
- mappings.push(
721
- get_mapping_from_node(
722
- closing,
723
- src_to_gen_map,
724
- gen_line_offsets,
725
- mapping_data_verify_only,
726
- ),
714
+ if (closing || opening.selfClosing) {
715
+ // Add the whole closing tag or the self-closing
716
+ const mapping = get_mapping_from_node(
717
+ closing ? closing : opening,
718
+ src_to_gen_map,
719
+ gen_line_offsets,
720
+ mapping_data_verify_only,
727
721
  );
728
722
 
723
+ // The generated code includes a semicolon after the closing or self-closed tag
724
+ // We're extending the mapping to include the semicolon
725
+ // because the diagnostics errors can include the whole element
726
+ // and we need to account for the semicolon as it's a part of the diagnostic
727
+ // At the same time, we could've instead applied this logic to the whole `node` element
728
+ // but since we already map the opening - start, we just need the proper end
729
+ // and it was causing some issues with mappings
730
+ mapping.generatedLengths = [mapping.generatedLengths[0] + 1];
731
+ mapping.data.customData.generatedLengths = mapping.generatedLengths;
732
+ mappings.push(mapping);
733
+ }
734
+
735
+ if (closing) {
729
736
  visit(closing);
730
737
  }
731
738
 
@@ -838,26 +845,86 @@ export function convert_source_map_to_mappings(
838
845
  if (node.test) {
839
846
  visit(node.test);
840
847
  }
848
+
841
849
  if (node.consequent) {
842
- mappings.push(
843
- get_mapping_from_node(
844
- node.consequent,
845
- src_to_gen_map,
846
- gen_line_offsets,
847
- mapping_data_verify_only,
848
- ),
849
- );
850
+ if (node.consequent.loc) {
851
+ // We're mapping only the brackets because mapping the whole thing
852
+ // would be way too broad and causes
853
+ // issues with partial mapping of something inside the body that we need
854
+ tokens.push(
855
+ {
856
+ source: '{',
857
+ generated: '{',
858
+ loc: {
859
+ start: {
860
+ line: node.consequent.loc.start.line,
861
+ column: node.consequent.loc.start.column,
862
+ },
863
+ end: {
864
+ line: node.consequent.loc.start.line,
865
+ column: node.consequent.loc.start.column + 1,
866
+ },
867
+ },
868
+ metadata: {},
869
+ mappingData: mapping_data_verify_only,
870
+ },
871
+ {
872
+ source: '}',
873
+ generated: '}',
874
+ loc: {
875
+ start: {
876
+ line: node.consequent.loc.end.line,
877
+ column: node.consequent.loc.end.column - 1,
878
+ },
879
+ end: {
880
+ line: node.consequent.loc.end.line,
881
+ column: node.consequent.loc.end.column,
882
+ },
883
+ },
884
+ metadata: {},
885
+ mappingData: mapping_data_verify_only,
886
+ },
887
+ );
888
+ }
889
+
850
890
  visit(node.consequent);
851
891
  }
892
+
852
893
  if (node.alternate) {
853
- mappings.push(
854
- get_mapping_from_node(
855
- node.alternate,
856
- src_to_gen_map,
857
- gen_line_offsets,
858
- mapping_data_verify_only,
859
- ),
860
- );
894
+ if (node.alternate.loc) {
895
+ tokens.push(
896
+ {
897
+ source: '{',
898
+ generated: '{',
899
+ loc: {
900
+ start: {
901
+ line: node.alternate.loc.start.line,
902
+ column: node.alternate.loc.start.column,
903
+ },
904
+ end: {
905
+ line: node.alternate.loc.start.line,
906
+ column: node.alternate.loc.start.column + 1,
907
+ },
908
+ },
909
+ metadata: {},
910
+ mappingData: mapping_data_verify_only,
911
+ },
912
+ {
913
+ source: '}',
914
+ generated: '}',
915
+ loc: {
916
+ start: {
917
+ line: node.alternate.loc.end.line,
918
+ column: node.alternate.loc.end.column - 1,
919
+ },
920
+ end: { line: node.alternate.loc.end.line, column: node.alternate.loc.end.column },
921
+ },
922
+ metadata: {},
923
+ mappingData: mapping_data_verify_only,
924
+ },
925
+ );
926
+ }
927
+
861
928
  visit(node.alternate);
862
929
  }
863
930
 
@@ -1198,6 +1265,23 @@ export function convert_source_map_to_mappings(
1198
1265
  if (node.argument) {
1199
1266
  visit(node.argument);
1200
1267
  }
1268
+
1269
+ if (node.type === 'ReturnStatement' && node.loc) {
1270
+ const mapping = get_mapping_from_node(
1271
+ node,
1272
+ src_to_gen_map,
1273
+ gen_line_offsets,
1274
+ mapping_data_verify_only,
1275
+ );
1276
+ // We're only mapping the 'return' keyword, otherwise the mapping would be too broad
1277
+ // and likely may cause issues with partial mappings of something inside the return statement that we need
1278
+ const return_keyword_length = 'return'.length;
1279
+ mapping.lengths = [return_keyword_length];
1280
+ mapping.generatedLengths = [return_keyword_length];
1281
+ mapping.data.customData.generatedLengths = mapping.generatedLengths;
1282
+
1283
+ mappings.push(mapping);
1284
+ }
1201
1285
  return;
1202
1286
  } else if (node.type === 'ExpressionStatement') {
1203
1287
  if (node.expression) {
@@ -24,8 +24,8 @@ import {
24
24
  is_void_element,
25
25
  normalize_children,
26
26
  is_binding_function,
27
- build_getter,
28
27
  is_element_dynamic,
28
+ hash,
29
29
  } from '../../../utils.js';
30
30
  import { escape } from '../../../../utils/escaping.js';
31
31
  import { is_event_attribute } from '../../../../utils/events.js';
@@ -38,6 +38,66 @@ import {
38
38
  } from '../../../identifier-utils.js';
39
39
  import { BLOCK_CLOSE, BLOCK_OPEN } from '../../../../constants.js';
40
40
 
41
+ /**
42
+ * Checks if a node is template or control-flow content that should be wrapped when return flags are active
43
+ * @param {AST.Node} node
44
+ * @returns {boolean}
45
+ */
46
+ function is_template_or_control_flow(node) {
47
+ return (
48
+ node.type === 'Element' ||
49
+ node.type === 'Text' ||
50
+ node.type === 'Html' ||
51
+ node.type === 'TsxCompat' ||
52
+ node.type === 'IfStatement' ||
53
+ node.type === 'ForOfStatement' ||
54
+ node.type === 'TryStatement' ||
55
+ node.type === 'SwitchStatement'
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Builds a negated AND condition from return flag names: !__r_1 && !__r_2 && ...
61
+ * @param {string[]} flags
62
+ * @returns {AST.Expression}
63
+ */
64
+ function build_return_guard(flags) {
65
+ /** @type {AST.Expression} */
66
+ let condition = b.unary('!', b.id(flags[0]));
67
+ for (let i = 1; i < flags.length; i++) {
68
+ condition = b.logical('&&', condition, b.unary('!', b.id(flags[i])));
69
+ }
70
+ return condition;
71
+ }
72
+
73
+ /**
74
+ * Collects all unique return statements from the direct children of a body
75
+ * @param {AST.Node[]} children
76
+ * @returns {AST.ReturnStatement[]}
77
+ */
78
+ function collect_returns_from_children(children) {
79
+ /** @type {AST.ReturnStatement[]} */
80
+ const returns = [];
81
+ const seen = new Set();
82
+ for (const node of children) {
83
+ if (node.type === 'ReturnStatement') {
84
+ if (!seen.has(node)) {
85
+ seen.add(node);
86
+ returns.push(node);
87
+ }
88
+ }
89
+ if (node.metadata?.returns) {
90
+ for (const ret of node.metadata.returns) {
91
+ if (!seen.has(ret)) {
92
+ seen.add(ret);
93
+ returns.push(ret);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return returns;
99
+ }
100
+
41
101
  /**
42
102
  * @param {AST.Node[]} children
43
103
  * @param {TransformServerContext} context
@@ -46,10 +106,45 @@ function transform_children(children, context) {
46
106
  const { visit, state } = context;
47
107
  const normalized = normalize_children(children, context);
48
108
 
49
- for (const node of normalized) {
109
+ const all_returns = collect_returns_from_children(normalized);
110
+ /** @type {Map<AST.ReturnStatement, { name: string, tracked: boolean }>} */
111
+ const return_flags = new Map([...(state.return_flags || [])]);
112
+ /** @type {AST.ReturnStatement[]} */
113
+ const new_returns = [];
114
+ for (const ret of all_returns) {
115
+ if (!return_flags.has(ret)) {
116
+ return_flags.set(ret, { name: state.scope.generate('__r'), tracked: false });
117
+ new_returns.push(ret);
118
+ }
119
+ }
120
+
121
+ for (const ret of new_returns) {
122
+ const info = /** @type {{ name: string, tracked: boolean }} */ (return_flags.get(ret));
123
+ state.init?.push(b.var(b.id(info.name), b.false));
124
+ }
125
+
126
+ // Track accumulated return flags as we process children
127
+ /** @type {string[]} */
128
+ let accumulated_flags = [];
129
+
130
+ /**
131
+ * @param {AST.ReturnStatement[] | undefined} returns
132
+ */
133
+ const push_return_flags = (returns) => {
134
+ if (!returns) return;
135
+ for (const ret of returns) {
136
+ const info = return_flags.get(ret);
137
+ if (info && !accumulated_flags.includes(info.name)) {
138
+ accumulated_flags.push(info.name);
139
+ }
140
+ }
141
+ };
142
+
143
+ /** @param {AST.Node} node */
144
+ const process_node = (node) => {
50
145
  if (node.type === 'BreakStatement') {
51
146
  state.init?.push(b.break);
52
- continue;
147
+ return;
53
148
  }
54
149
  if (
55
150
  node.type === 'VariableDeclaration' ||
@@ -60,21 +155,88 @@ function transform_children(children, context) {
60
155
  node.type === 'ClassDeclaration' ||
61
156
  node.type === 'TSTypeAliasDeclaration' ||
62
157
  node.type === 'TSInterfaceDeclaration' ||
158
+ node.type === 'ReturnStatement' ||
63
159
  node.type === 'Component'
64
160
  ) {
65
161
  const metadata = { await: false };
66
- state.init?.push(/** @type {AST.Statement} */ (visit(node, { ...state, metadata })));
162
+ state.init?.push(
163
+ /** @type {AST.Statement} */ (visit(node, { ...state, return_flags, metadata })),
164
+ );
67
165
  if (metadata.await) {
68
166
  state.init?.push(b.if(b.call('_$_.aborted'), b.return(null)));
69
167
  if (state.metadata?.await === false) {
70
168
  state.metadata.await = true;
71
169
  }
72
170
  }
171
+ if (node.type === 'ReturnStatement') {
172
+ const info = return_flags.get(node);
173
+ if (info && !accumulated_flags.includes(info.name)) {
174
+ accumulated_flags.push(info.name);
175
+ }
176
+ }
73
177
  } else {
74
- visit(node, state);
178
+ visit(node, { ...state, return_flags });
179
+ }
180
+ };
181
+
182
+ /** @type {AST.Node[]} */
183
+ let pending_group = [];
184
+ /** @type {string[]} */
185
+ let pending_guard_flags = [];
186
+
187
+ const flush_pending_group = () => {
188
+ if (pending_group.length === 0) return;
189
+
190
+ const group = pending_group;
191
+ const guard_flags = pending_guard_flags;
192
+ pending_group = [];
193
+ pending_guard_flags = [];
194
+
195
+ /** @type {AST.Statement[]} */
196
+ const wrapped = [];
197
+ const saved_init = state.init;
198
+ state.init = wrapped;
199
+
200
+ for (const group_node of group) {
201
+ process_node(group_node);
202
+ }
203
+
204
+ state.init = saved_init;
205
+ if (wrapped.length === 0) return;
206
+
207
+ const guard = build_return_guard(guard_flags);
208
+ state.init?.push(
209
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(BLOCK_OPEN))),
210
+ );
211
+ state.init?.push(b.if(guard, b.block(wrapped)));
212
+ state.init?.push(
213
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(BLOCK_CLOSE))),
214
+ );
215
+ };
216
+
217
+ for (let idx = 0; idx < normalized.length; idx++) {
218
+ const node = normalized[idx];
219
+
220
+ if (accumulated_flags.length > 0 && is_template_or_control_flow(node)) {
221
+ if (pending_group.length === 0) {
222
+ pending_guard_flags = [...accumulated_flags];
223
+ }
224
+ pending_group.push(node);
225
+
226
+ if (node.metadata?.has_return && node.metadata.returns) {
227
+ flush_pending_group();
228
+ push_return_flags(node.metadata.returns);
229
+ }
230
+ continue;
75
231
  }
232
+
233
+ flush_pending_group();
234
+ process_node(node);
235
+ push_return_flags(node.metadata?.has_return ? node.metadata.returns : undefined);
76
236
  }
77
237
 
238
+ flush_pending_group();
239
+
78
240
  const head_elements = /** @type {AST.Element[]} */ (
79
241
  children.filter(
80
242
  (node) => node.type === 'Element' && node.id.type === 'Identifier' && node.id.name === 'head',
@@ -85,8 +247,21 @@ function transform_children(children, context) {
85
247
  state.init?.push(
86
248
  b.stmt(b.assignment('=', b.member(b.id('__output'), b.id('target')), b.literal('head'))),
87
249
  );
88
- for (const head_element of head_elements) {
250
+ for (let i = 0; i < head_elements.length; i++) {
251
+ const head_element = head_elements[i];
252
+ // Generate a hash for this head element to match client-side hydration
253
+ // Use both filename and index to ensure uniqueness
254
+ const hash_source = `${context.state.filename}:head:${i}:${head_element.start ?? 0}`;
255
+ const hash_value = hash(hash_source);
256
+
257
+ // Emit hydration marker comment with hash
258
+ state.init?.push(
259
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(`<!--${hash_value}-->`))),
260
+ );
261
+
89
262
  transform_children(head_element.children, context);
263
+
264
+ // No closing marker needed for head elements - the hash is sufficient
90
265
  }
91
266
  state.init?.push(
92
267
  b.stmt(b.assignment('=', b.member(b.id('__output'), b.id('target')), b.literal(null))),
@@ -504,6 +679,7 @@ const visitors = {
504
679
  const is_void = dynamic_name
505
680
  ? false
506
681
  : is_void_element(/** @type {AST.Identifier} */ (node.id).name);
682
+ const use_self_closing_syntax = node.selfClosing && (is_void || !!dynamic_name);
507
683
  const tag_name = dynamic_name
508
684
  ? dynamic_name
509
685
  : b.literal(/** @type {AST.Identifier} */ (node.id).name);
@@ -659,11 +835,28 @@ const visitors = {
659
835
  b.stmt(
660
836
  b.call(
661
837
  b.member(b.id('__output'), b.id('push')),
662
- b.literal(!node.selfClosing ? '>' : ' />'),
838
+ b.literal(use_self_closing_syntax ? ' />' : '>'),
663
839
  ),
664
840
  ),
665
841
  );
666
842
 
843
+ // In dev mode, emit push_element for runtime nesting validation
844
+ if (state.dev && !dynamic_name) {
845
+ const element_name = /** @type {AST.Identifier} */ (node.id).name;
846
+ const loc = node.loc;
847
+ state.init?.push(
848
+ b.stmt(
849
+ b.call(
850
+ '_$_.push_element',
851
+ b.literal(element_name),
852
+ b.literal(state.filename),
853
+ b.literal(loc?.start.line ?? 0),
854
+ b.literal(loc?.start.column ?? 0),
855
+ ),
856
+ ),
857
+ );
858
+ }
859
+
667
860
  if (!is_void) {
668
861
  /** @type {AST.Statement[]} */
669
862
  const init = [];
@@ -690,7 +883,7 @@ const visitors = {
690
883
  state.init?.push(b.block(init));
691
884
  }
692
885
 
693
- if (!node.selfClosing) {
886
+ if (!use_self_closing_syntax) {
694
887
  state.init?.push(
695
888
  b.stmt(
696
889
  b.call(
@@ -703,6 +896,11 @@ const visitors = {
703
896
  );
704
897
  }
705
898
  }
899
+
900
+ // In dev mode, emit pop_element after the element is fully rendered
901
+ if (state.dev && !dynamic_name) {
902
+ state.init?.push(b.stmt(b.call('_$_.pop_element')));
903
+ }
706
904
  } else {
707
905
  /** @type {(AST.Property | AST.SpreadElement)[]} */
708
906
  const props = [];
@@ -904,9 +1102,17 @@ const visitors = {
904
1102
  );
905
1103
  }
906
1104
 
1105
+ context.state.init?.push(
1106
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(BLOCK_OPEN))),
1107
+ );
1108
+
907
1109
  context.state.init?.push(
908
1110
  b.switch(/** @type {AST.Expression} */ (context.visit(node.discriminant)), cases),
909
1111
  );
1112
+
1113
+ context.state.init?.push(
1114
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(BLOCK_CLOSE))),
1115
+ );
910
1116
  },
911
1117
 
912
1118
  ForOfStatement(node, context) {
@@ -995,6 +1201,17 @@ const visitors = {
995
1201
  );
996
1202
  },
997
1203
 
1204
+ ReturnStatement(node, context) {
1205
+ if (!is_inside_component(context)) {
1206
+ return context.next();
1207
+ }
1208
+ const info = context.state.return_flags?.get(node);
1209
+ if (info) {
1210
+ return b.stmt(b.assignment('=', b.id(info.name), b.true));
1211
+ }
1212
+ return context.next();
1213
+ },
1214
+
998
1215
  AssignmentExpression(node, context) {
999
1216
  const left = node.left;
1000
1217
 
@@ -1296,14 +1513,48 @@ const visitors = {
1296
1513
  visit(node.expression, { ...state, metadata })
1297
1514
  );
1298
1515
 
1299
- // For Html nodes, we render the content as-is without escaping
1516
+ // For literal values, compute hash at build time
1300
1517
  if (expression.type === 'Literal') {
1518
+ const value = String(expression.value ?? '');
1519
+ const hash_value = hash(value);
1520
+ // Push hash comment
1521
+ state.init?.push(
1522
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(`<!--${hash_value}-->`))),
1523
+ );
1524
+ // Push the HTML content
1525
+ state.init?.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(value))));
1526
+ // Push empty comment as end marker
1301
1527
  state.init?.push(
1302
- b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(expression.value))),
1528
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal('<!---->'))),
1303
1529
  );
1304
1530
  } else {
1305
- // If it's dynamic, we need to evaluate it and push it directly (not escaped)
1306
- state.init?.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), expression)));
1531
+ // For dynamic values, compute hash at runtime
1532
+ // Create a variable to store the value
1533
+ const value_id = state.scope?.generate('html_value');
1534
+ if (value_id) {
1535
+ state.init?.push(
1536
+ b.const(value_id, b.call(b.id('String'), b.logical('??', expression, b.literal('')))),
1537
+ );
1538
+ // Compute hash at runtime using _$_.hash and push as comment
1539
+ state.init?.push(
1540
+ b.stmt(
1541
+ b.call(
1542
+ b.member(b.id('__output'), b.id('push')),
1543
+ b.binary(
1544
+ '+',
1545
+ b.binary('+', b.literal('<!--'), b.call('_$_.hash', b.id(value_id))),
1546
+ b.literal('-->'),
1547
+ ),
1548
+ ),
1549
+ ),
1550
+ );
1551
+ // Push the HTML content
1552
+ state.init?.push(b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.id(value_id))));
1553
+ // Push empty comment as end marker
1554
+ state.init?.push(
1555
+ b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal('<!---->'))),
1556
+ );
1557
+ }
1307
1558
  }
1308
1559
  },
1309
1560
 
@@ -1390,9 +1641,10 @@ const visitors = {
1390
1641
  * @param {string} source
1391
1642
  * @param {AnalysisResult} analysis
1392
1643
  * @param {boolean} minify_css
1644
+ * @param {boolean} [dev]
1393
1645
  * @returns {{ ast: AST.Program; js: { code: string; map: RawSourceMap | null }; css: string; }}
1394
1646
  */
1395
- export function transform_server(filename, source, analysis, minify_css) {
1647
+ export function transform_server(filename, source, analysis, minify_css, dev = false) {
1396
1648
  // Use component metadata collected during the analyze phase
1397
1649
  const component_metadata = analysis.component_metadata || [];
1398
1650
 
@@ -1413,6 +1665,7 @@ export function transform_server(filename, source, analysis, minify_css) {
1413
1665
  // TODO: should we remove all `to_ts` usages we use the client rendering for that?
1414
1666
  to_ts: false,
1415
1667
  metadata: {},
1668
+ dev,
1416
1669
  };
1417
1670
 
1418
1671
  state.imports.add(`import * as _$_ from 'ripple/internal/server'`);
@@ -26,6 +26,10 @@ interface BaseNodeMetaData {
26
26
  parenthesized?: boolean;
27
27
  elementLeadingComments?: AST.Comment[];
28
28
  inside_component_top_level?: boolean;
29
+ returns?: AST.ReturnStatement[];
30
+ has_return?: boolean;
31
+ is_reactive?: boolean;
32
+ lone_return?: boolean;
29
33
  }
30
34
 
31
35
  interface FunctionMetaData extends BaseNodeMetaData {
@@ -95,14 +99,18 @@ declare module 'estree' {
95
99
 
96
100
  // These 3 are needed so that Literal can extend TrackedNode
97
101
  // since Literal is a union type we have to extend each individually
98
- interface SimpleLiteral extends AST.TrackedNode {}
99
- interface RegExpLiteral extends AST.TrackedNode {}
100
- interface BigIntLiteral extends AST.TrackedNode {}
102
+ interface SimpleLiteral extends AST.LiteralTrackedNode {}
103
+ interface RegExpLiteral extends AST.LiteralTrackedNode {}
104
+ interface BigIntLiteral extends AST.LiteralTrackedNode {}
101
105
 
102
106
  interface TrackedNode {
103
107
  tracked?: boolean;
104
108
  }
105
109
 
110
+ interface LiteralTrackedNode extends AST.TrackedNode {
111
+ was_expression?: boolean;
112
+ }
113
+
106
114
  // Include TypeScript node types and Ripple-specific nodes in NodeMap
107
115
  interface NodeMap {
108
116
  Component: Component;
@@ -132,6 +140,7 @@ declare module 'estree' {
132
140
  ServerIdentifier: ServerIdentifier;
133
141
  Text: TextNode;
134
142
  JSXEmptyExpression: ESTreeJSX.JSXEmptyExpression;
143
+ ParenthesizedExpression: ParenthesizedExpression;
135
144
  }
136
145
 
137
146
  // Missing estree type
@@ -1212,6 +1221,8 @@ export interface TransformServerState extends BaseState {
1212
1221
  server_exported_names: string[];
1213
1222
  dynamicElementName?: AST.TemplateLiteral;
1214
1223
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1224
+ dev?: boolean;
1225
+ return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1215
1226
  }
1216
1227
 
1217
1228
  type UpdateList = Array<
@@ -1243,6 +1254,8 @@ export interface TransformClientState extends BaseState {
1243
1254
  update: UpdateList | null;
1244
1255
  errors: RippleCompileError[];
1245
1256
  applyParentCssScope?: AST.CSS.StyleSheet['hash'];
1257
+ skip_children_traversal: boolean;
1258
+ return_flags?: Map<AST.ReturnStatement, { name: string; tracked: boolean }>;
1246
1259
  }
1247
1260
 
1248
1261
  /** Override zimmerframe types and provide our own */
@@ -7,7 +7,7 @@ import { build_assignment_value, extract_paths } from '../utils/ast.js';
7
7
  import * as b from '../utils/builders.js';
8
8
  import { is_capture_event, is_non_delegated, normalize_event_name } from '../utils/events.js';
9
9
 
10
- const regex_return_characters = /\r/g;
10
+ export { hash } from '../utils/hashing.js';
11
11
 
12
12
  const VOID_ELEMENT_NAMES = [
13
13
  'area',
@@ -555,20 +555,6 @@ export function escape_html(value, is_attr = false) {
555
555
  return escaped + str.substring(last);
556
556
  }
557
557
 
558
- /**
559
- * Hashes a string to a base36 value
560
- * @param {string} str
561
- * @returns {string}
562
- */
563
- export function hash(str) {
564
- str = str.replace(regex_return_characters, '');
565
- let hash = 5381;
566
- let i = str.length;
567
-
568
- while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i);
569
- return (hash >>> 0).toString(36);
570
- }
571
-
572
558
  /**
573
559
  * Returns true if node is a DOM element (not a component)
574
560
  * @param {AST.Node} node
package/src/constants.js CHANGED
@@ -6,12 +6,10 @@ export const TEMPLATE_SVG_NAMESPACE = 1 << 5;
6
6
  export const TEMPLATE_MATHML_NAMESPACE = 1 << 6;
7
7
 
8
8
  export const HYDRATION_START = '[';
9
- export const HYDRATION_START_ELSE = '[!';
10
9
  export const HYDRATION_END = ']';
11
10
  export const HYDRATION_ERROR = {};
12
11
 
13
12
  export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
14
- export const BLOCK_OPEN_ELSE = `<!--${HYDRATION_START_ELSE}-->`;
15
13
  export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
16
14
  export const EMPTY_COMMENT = `<!---->`;
17
15