ripple 0.2.152 → 0.2.154

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 (67) hide show
  1. package/README.md +3 -3
  2. package/package.json +5 -5
  3. package/src/compiler/phases/1-parse/index.js +1 -1
  4. package/src/compiler/phases/3-transform/client/index.js +37 -16
  5. package/src/compiler/phases/3-transform/server/index.js +43 -25
  6. package/src/runtime/internal/client/events.js +5 -1
  7. package/src/runtime/internal/client/index.js +2 -1
  8. package/src/runtime/internal/client/render.js +18 -15
  9. package/src/runtime/internal/client/runtime.js +75 -10
  10. package/src/runtime/internal/server/index.js +51 -11
  11. package/src/server/index.js +1 -1
  12. package/tests/client/array/array.derived.test.ripple +61 -33
  13. package/tests/client/array/array.iteration.test.ripple +3 -1
  14. package/tests/client/array/array.mutations.test.ripple +19 -15
  15. package/tests/client/array/array.static.test.ripple +115 -104
  16. package/tests/client/array/array.to-methods.test.ripple +3 -3
  17. package/tests/client/basic/basic.attributes.test.ripple +110 -57
  18. package/tests/client/basic/basic.collections.test.ripple +41 -22
  19. package/tests/client/basic/basic.errors.test.ripple +12 -6
  20. package/tests/client/basic/basic.events.test.ripple +51 -33
  21. package/tests/client/basic/basic.reactivity.test.ripple +120 -56
  22. package/tests/client/basic/basic.rendering.test.ripple +49 -19
  23. package/tests/client/basic/basic.styling.test.ripple +2 -2
  24. package/tests/client/basic/basic.utilities.test.ripple +1 -1
  25. package/tests/client/boundaries.test.ripple +70 -58
  26. package/tests/client/compiler/compiler.assignments.test.ripple +32 -4
  27. package/tests/client/compiler/compiler.attributes.test.ripple +46 -46
  28. package/tests/client/compiler/compiler.basic.test.ripple +18 -15
  29. package/tests/client/compiler/compiler.tracked-access.test.ripple +53 -42
  30. package/tests/client/compiler/compiler.typescript.test.ripple +1 -2
  31. package/tests/client/composite/composite.dynamic-components.test.ripple +6 -6
  32. package/tests/client/composite/composite.generics.test.ripple +39 -36
  33. package/tests/client/composite/composite.props.test.ripple +4 -3
  34. package/tests/client/composite/composite.reactivity.test.ripple +112 -27
  35. package/tests/client/composite/composite.render.test.ripple +9 -8
  36. package/tests/client/computed-properties.test.ripple +24 -24
  37. package/tests/client/context.test.ripple +11 -9
  38. package/tests/client/date.test.ripple +3 -1
  39. package/tests/client/dynamic-elements.test.ripple +103 -78
  40. package/tests/client/for.test.ripple +27 -17
  41. package/tests/client/head.test.ripple +42 -6
  42. package/tests/client/html.test.ripple +42 -32
  43. package/tests/client/input-value.test.ripple +4 -4
  44. package/tests/client/map.test.ripple +140 -141
  45. package/tests/client/media-query.test.ripple +31 -31
  46. package/tests/client/object.test.ripple +148 -112
  47. package/tests/client/portal.test.ripple +29 -15
  48. package/tests/client/ref.test.ripple +9 -3
  49. package/tests/client/set.test.ripple +111 -111
  50. package/tests/client/tracked-expression.test.ripple +16 -17
  51. package/tests/client/url/url.derived.test.ripple +19 -9
  52. package/tests/client/url/url.parsing.test.ripple +24 -8
  53. package/tests/client/url/url.partial-removal.test.ripple +12 -4
  54. package/tests/client/url/url.reactivity.test.ripple +63 -25
  55. package/tests/client/url/url.serialization.test.ripple +18 -6
  56. package/tests/client/url-search-params/url-search-params.derived.test.ripple +10 -6
  57. package/tests/client/url-search-params/url-search-params.iteration.test.ripple +3 -1
  58. package/tests/client/url-search-params/url-search-params.mutation.test.ripple +26 -14
  59. package/tests/client/url-search-params/url-search-params.tracked-url.test.ripple +3 -1
  60. package/tests/server/await.test.ripple +23 -22
  61. package/tests/server/basic.test.ripple +1 -1
  62. package/tests/server/compiler.test.ripple +3 -7
  63. package/tests/server/composite.test.ripple +38 -36
  64. package/tests/server/for.test.ripple +9 -5
  65. package/tests/server/if.test.ripple +1 -1
  66. package/tests/server/streaming-ssr.test.ripple +67 -0
  67. package/types/server.d.ts +5 -4
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <picture>
2
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/trueadm/ripple/master/assets/ripple-dark.png">
3
- <img src="https://raw.githubusercontent.com/trueadm/ripple/master/assets/ripple-light.png" alt="Ripple - the elegant TypeScript UI framework" />
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Ripple-TS/ripple/master/assets/ripple-dark.png">
3
+ <img src="https://raw.githubusercontent.com/Ripple-TS/ripple/master/assets/ripple-light.png" alt="Ripple - the elegant TypeScript UI framework" />
4
4
  </picture>
5
5
 
6
6
  # What is Ripple?
7
7
 
8
- Ripple is an elegant TypeScript UI framework. To find out more, view [Ripple's Github README](https://github.com/trueadm/ripple).
8
+ Ripple is an elegant TypeScript UI framework. To find out more, view [Ripple's Github README](https://github.com/Ripple-TS/ripple).
package/package.json CHANGED
@@ -3,19 +3,19 @@
3
3
  "description": "Ripple is an elegant TypeScript UI framework",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.2.152",
6
+ "version": "0.2.154",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/trueadm/ripple.git",
12
+ "url": "git+https://github.com/Ripple-TS/ripple.git",
13
13
  "directory": "packages/ripple"
14
14
  },
15
15
  "bugs": {
16
- "url": "https://github.com/trueadm/ripple/issues"
16
+ "url": "https://github.com/Ripple-TS/ripple/issues"
17
17
  },
18
- "homepage": "https://ripplejs.com",
18
+ "homepage": "https://ripple-ts.com",
19
19
  "keywords": [
20
20
  "ripple",
21
21
  "UI",
@@ -81,6 +81,6 @@
81
81
  "typescript": "^5.9.2"
82
82
  },
83
83
  "peerDependencies": {
84
- "ripple": "0.2.152"
84
+ "ripple": "0.2.154"
85
85
  }
86
86
  }
@@ -1795,7 +1795,7 @@ function RipplePlugin(config) {
1795
1795
  /**
1796
1796
  * Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
1797
1797
  * to add them after the fact. They are needed in order to support `ripple-ignore` comments
1798
- * in JS code and so that `prettier-plugin-ripple` doesn't remove all comments when formatting.
1798
+ * in JS code and so that `prettier-plugin` doesn't remove all comments when formatting.
1799
1799
  * @param {string} source
1800
1800
  * @param {CommentWithLocation[]} comments
1801
1801
  * @param {number} [index=0] - Starting index
@@ -1240,29 +1240,50 @@ const visitors = {
1240
1240
  // We're calling a component from within svg/mathml context
1241
1241
  const is_with_ns = state.namespace !== DEFAULT_NAMESPACE;
1242
1242
 
1243
+ let object_props;
1244
+ if (is_spreading) {
1245
+ // Optimization: if only one spread with no other props, pass it directly
1246
+ if (props.length === 1 && props[0].type === 'SpreadElement') {
1247
+ object_props = b.call('_$_.spread_props', b.thunk(props[0].argument));
1248
+ } else {
1249
+ // Multiple items: build array of objects/spreads for proper merge order
1250
+ const items = [];
1251
+ let current_obj_props = [];
1252
+
1253
+ for (const prop of props) {
1254
+ if (prop.type === 'SpreadElement') {
1255
+ // Flush accumulated regular props as an object
1256
+ if (current_obj_props.length > 0) {
1257
+ items.push(b.object(current_obj_props));
1258
+ current_obj_props = [];
1259
+ }
1260
+ // Add the spread argument directly
1261
+ items.push(prop.argument);
1262
+ } else {
1263
+ // Accumulate regular properties
1264
+ current_obj_props.push(prop);
1265
+ }
1266
+ }
1267
+
1268
+ // Flush any remaining regular props
1269
+ if (current_obj_props.length > 0) {
1270
+ items.push(b.object(current_obj_props));
1271
+ }
1272
+
1273
+ object_props = b.call('_$_.spread_props', b.thunk(b.array(items)));
1274
+ }
1275
+ } else {
1276
+ object_props = b.object(props);
1277
+ }
1243
1278
  if (metadata.tracking) {
1244
- const shared = b.call(
1245
- '_$_.composite',
1246
- b.thunk(visit(node.id, state)),
1247
- id,
1248
- is_spreading
1249
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1250
- : b.object(props),
1251
- );
1279
+ const shared = b.call('_$_.composite', b.thunk(visit(node.id, state)), id, object_props);
1252
1280
  state.init.push(
1253
1281
  is_with_ns
1254
1282
  ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
1255
1283
  : b.stmt(shared),
1256
1284
  );
1257
1285
  } else {
1258
- const shared = b.call(
1259
- visit(node.id, state),
1260
- id,
1261
- is_spreading
1262
- ? b.call('_$_.spread_props', b.thunk(b.object(props)), b.id('__block'))
1263
- : b.object(props),
1264
- b.id('_$_.active_block'),
1265
- );
1286
+ const shared = b.call(visit(node.id, state), id, object_props, b.id('_$_.active_block'));
1266
1287
  state.init.push(
1267
1288
  is_with_ns
1268
1289
  ? b.call('_$_.with_ns', b.literal(state.namespace), b.thunk(shared))
@@ -120,14 +120,23 @@ const visitors = {
120
120
  component_fn = b.async(component_fn);
121
121
  }
122
122
 
123
- const declaration = b.function_declaration(node.id, component_fn.params, component_fn.body, component_fn.async);
123
+ const declaration = b.function_declaration(
124
+ node.id,
125
+ component_fn.params,
126
+ component_fn.body,
127
+ component_fn.async,
128
+ );
124
129
 
125
130
  if (metadata.await) {
126
131
  const parent = context.path.at(-1);
127
132
  if (parent.type === 'Program' || parent.type === 'BlockStatement') {
128
133
  const body = parent.body;
129
134
  const index = body.indexOf(node);
130
- body.splice(index + 1, 0, b.stmt(b.assignment('=', b.member(node.id, b.id('async')), b.true)));
135
+ body.splice(
136
+ index + 1,
137
+ 0,
138
+ b.stmt(b.assignment('=', b.member(node.id, b.id('async')), b.true)),
139
+ );
131
140
  }
132
141
  }
133
142
 
@@ -281,10 +290,11 @@ const visitors = {
281
290
  let class_attribute = null;
282
291
 
283
292
  const handle_static_attr = (name, value) => {
284
- const attr_str = ` ${name}${is_boolean_attribute(name) && value === true
293
+ const attr_str = ` ${name}${
294
+ is_boolean_attribute(name) && value === true
285
295
  ? ''
286
296
  : `="${value === true ? '' : escape_html(value, true)}"`
287
- }`;
297
+ }`;
288
298
 
289
299
  if (is_spreading) {
290
300
  // For spread attributes, store just the actual value, not the full attribute string
@@ -566,15 +576,13 @@ const visitors = {
566
576
  return b.call(
567
577
  'set',
568
578
  context.visit(left, { ...context.state, metadata: { tracking: false } }),
569
- context.visit(node.right)
579
+ context.visit(node.right),
570
580
  );
571
581
  }
572
582
 
573
583
  return context.next();
574
584
  },
575
585
 
576
-
577
-
578
586
  ServerIdentifier(node, context) {
579
587
  return b.id('_$_server_$_');
580
588
  },
@@ -617,27 +625,36 @@ const visitors = {
617
625
  context.state.metadata.await = true;
618
626
  }
619
627
 
628
+ // Render pending block first
629
+ if (node.pending) {
630
+ const pending_body = transform_body(node.pending.body, {
631
+ ...context,
632
+ state: { ...context.state, scope: context.state.scopes.get(node.pending) },
633
+ });
634
+ context.state.init.push(...pending_body);
635
+ }
636
+
620
637
  // For SSR with pending block: render the resolved content wrapped in async
621
638
  // In a streaming SSR implementation, we'd render pending first, then stream resolved
622
639
  const try_statements =
623
640
  node.handler !== null
624
641
  ? [
625
- b.try(
626
- b.block(body),
627
- b.catch_clause(
628
- node.handler.param || b.id('error'),
629
- b.block(
630
- transform_body(node.handler.body.body, {
631
- ...context,
632
- state: {
633
- ...context.state,
634
- scope: context.state.scopes.get(node.handler.body),
635
- },
636
- }),
642
+ b.try(
643
+ b.block(body),
644
+ b.catch_clause(
645
+ node.handler.param || b.id('error'),
646
+ b.block(
647
+ transform_body(node.handler.body.body, {
648
+ ...context,
649
+ state: {
650
+ ...context.state,
651
+ scope: context.state.scopes.get(node.handler.body),
652
+ },
653
+ }),
654
+ ),
637
655
  ),
638
656
  ),
639
- ),
640
- ]
657
+ ]
641
658
  : body;
642
659
 
643
660
  context.state.init.push(
@@ -664,8 +681,8 @@ const visitors = {
664
681
  },
665
682
 
666
683
  AwaitExpression(node, context) {
667
- context.state.scope.server_block = true
668
- context.inside_server_block = true
684
+ context.state.scope.server_block = true;
685
+ context.inside_server_block = true;
669
686
  if (context.state.to_ts) {
670
687
  return context.next();
671
688
  }
@@ -715,7 +732,9 @@ const visitors = {
715
732
 
716
733
  if (expression.type === 'Literal') {
717
734
  state.init.push(
718
- b.stmt(b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value)))),
735
+ b.stmt(
736
+ b.call(b.member(b.id('__output'), b.id('push')), b.literal(escape(expression.value))),
737
+ ),
719
738
  );
720
739
  } else {
721
740
  state.init.push(
@@ -810,7 +829,6 @@ export function transform_server(filename, source, analysis) {
810
829
  }
811
830
 
812
831
  // Add async property to component functions
813
-
814
832
 
815
833
  for (const import_node of state.imports) {
816
834
  program.body.unshift(b.stmt(b.id(import_node)));
@@ -29,6 +29,8 @@ export function on(element, type, handler, options = {}) {
29
29
  };
30
30
  }
31
31
 
32
+ let last_propagated_event = null;
33
+
32
34
  /**
33
35
  * @this {EventTarget}
34
36
  * @param {Event} event
@@ -41,6 +43,8 @@ export function handle_event_propagation(event) {
41
43
  var path = event.composedPath?.() || [];
42
44
  var current_target = /** @type {null | Element} */ (path[0] || event.target);
43
45
 
46
+ last_propagated_event = event;
47
+
44
48
  // composedPath contains list of nodes the event has propagated through.
45
49
  // We check __root to skip all nodes below it in case this is a
46
50
  // parent of the __root node, which indicates that there's nested
@@ -48,7 +52,7 @@ export function handle_event_propagation(event) {
48
52
  var path_idx = 0;
49
53
 
50
54
  // @ts-expect-error is added below
51
- var handled_at = event.__root;
55
+ var handled_at = last_propagated_event === event && event.__root;
52
56
 
53
57
  if (handled_at) {
54
58
  var at_idx = path.indexOf(handled_at);
@@ -47,9 +47,10 @@ export {
47
47
  derived,
48
48
  maybe_tracked,
49
49
  tick,
50
- proxy_tracked,
50
+ proxy_props,
51
51
  with_block,
52
52
  with_ns,
53
+ handle_error,
53
54
  } from './runtime.js';
54
55
 
55
56
  export { composite } from './composite.js';
@@ -124,17 +124,21 @@ export function apply_styles(element, newStyles) {
124
124
 
125
125
  /**
126
126
  * @param {Element} element
127
- * @param {Record<string, any>} attributes
127
+ * @param {Record<string, any>} prev
128
+ * @param {Record<string, any>} next
128
129
  * @returns {void}
129
130
  */
130
- export function set_attributes(element, attributes) {
131
+ export function set_attributes(element, prev, next) {
131
132
  let found_enumerable_keys = false;
132
133
 
133
- for (const key in attributes) {
134
+ for (const key in next) {
134
135
  if (key === 'children') continue;
135
136
  found_enumerable_keys = true;
136
137
 
137
- let value = attributes[key];
138
+ let value = next[key];
139
+ if (prev[key] === value && key !== '#class') {
140
+ continue;
141
+ }
138
142
  if (is_tracked_object(value)) {
139
143
  value = get(value);
140
144
  }
@@ -143,13 +147,16 @@ export function set_attributes(element, attributes) {
143
147
 
144
148
  // Only if no enumerable keys but attributes object exists
145
149
  // This handles spread_props Proxy objects from dynamic elements with {...spread}
146
- if (!found_enumerable_keys && attributes) {
147
- const allKeys = Reflect.ownKeys(attributes);
150
+ if (!found_enumerable_keys && next) {
151
+ const allKeys = Reflect.ownKeys(next);
148
152
  for (const key of allKeys) {
149
153
  if (key === 'children') continue;
150
154
  if (typeof key === 'symbol') continue; // Skip symbols - handled by apply_element_spread
151
155
 
152
- let value = attributes[key];
156
+ let value = next[key];
157
+ if (prev[key] === value && key !== '#class') {
158
+ continue;
159
+ }
153
160
  if (is_tracked_object(value)) {
154
161
  value = get(value);
155
162
  }
@@ -285,13 +292,13 @@ export function set_selected(element, selected) {
285
292
  * @returns {() => void}
286
293
  */
287
294
  export function apply_element_spread(element, fn) {
288
- /** @type {Record<string | symbol, any> | undefined} */
289
- var prev;
295
+ /** @type {Record<string | symbol, any>} */
296
+ var prev = {};
290
297
  /** @type {Record<symbol, Block>} */
291
298
  var effects = {};
292
299
 
293
300
  return () => {
294
- var next = fn();
301
+ var next = { ...fn() };
295
302
 
296
303
  for (let symbol of get_own_property_symbols(effects)) {
297
304
  if (!next[symbol]) {
@@ -300,10 +307,6 @@ export function apply_element_spread(element, fn) {
300
307
  }
301
308
 
302
309
  for (const symbol of get_own_property_symbols(next)) {
303
- // Ensure we are not trying to write to a proxied object
304
- if (TRACKED_OBJECT in next) {
305
- next = { ...next };
306
- }
307
310
  var ref_fn = next[symbol];
308
311
 
309
312
  if (symbol.description === REF_PROP && (!prev || ref_fn !== prev[symbol])) {
@@ -316,7 +319,7 @@ export function apply_element_spread(element, fn) {
316
319
  next[symbol] = ref_fn;
317
320
  }
318
321
 
319
- set_attributes(element, next);
322
+ set_attributes(element, prev, next);
320
323
 
321
324
  prev = next;
322
325
  };
@@ -922,42 +922,107 @@ export function flush_sync(fn) {
922
922
 
923
923
  /**
924
924
  * @param {() => Object} fn
925
- * @param {Block} block
926
925
  * @returns {Object}
927
926
  */
928
- export function spread_props(fn, block) {
929
- var computed = derived(fn, block);
930
- return proxy_tracked(computed);
927
+ export function spread_props(fn) {
928
+ return proxy_props(fn);
931
929
  }
932
930
 
933
931
  /**
934
- * @param {Tracked | Derived} tracked
932
+ * @param {() => Object} fn
935
933
  * @returns {Object}
936
934
  */
937
- export function proxy_tracked(tracked) {
935
+ export function proxy_props(fn) {
938
936
  return new Proxy(
939
937
  {},
940
938
  {
941
939
  get(_, property) {
942
- const obj = get(tracked);
940
+ /** @type {Record<string | symbol, any> | Record<string | symbol, any>[]} */
941
+ var obj = fn();
942
+
943
+ // Handle array of objects/spreads (for multiple props)
944
+ if (is_array(obj)) {
945
+ // Search in reverse order (right-to-left) since later props override earlier ones
946
+ /** @type {Record<string | symbol, any>} */
947
+ var item;
948
+ for (var i = obj.length - 1; i >= 0; i--) {
949
+ item = obj[i];
950
+ if (property in item) {
951
+ return item[property];
952
+ }
953
+ }
954
+ return undefined;
955
+ }
956
+
957
+ // Single object case
943
958
  return obj[property];
944
959
  },
945
960
  has(_, property) {
946
961
  if (property === TRACKED_OBJECT) {
947
962
  return true;
948
963
  }
949
- const obj = get(tracked);
964
+ /** @type {Record<string | symbol, any> | Record<string | symbol, any>[]} */
965
+ var obj = fn();
966
+
967
+ // Handle array of objects/spreads
968
+ if (is_array(obj)) {
969
+ for (var i = obj.length - 1; i >= 0; i--) {
970
+ if (property in obj[i]) {
971
+ return true;
972
+ }
973
+ }
974
+ return false;
975
+ }
976
+
950
977
  return property in obj;
951
978
  },
952
979
  getOwnPropertyDescriptor(_, key) {
953
- const obj = get(tracked);
980
+ /** @type {Record<string | symbol, any> | Record<string | symbol, any>[]} */
981
+ var obj = fn();
982
+
983
+ // Handle array of objects/spreads
984
+ if (is_array(obj)) {
985
+ /** @type {Record<string | symbol, any>} */
986
+ var item;
987
+ for (var i = obj.length - 1; i >= 0; i--) {
988
+ item = obj[i];
989
+ if (key in item) {
990
+ return get_descriptor(item, key);
991
+ }
992
+ }
993
+ return undefined;
994
+ }
954
995
 
955
996
  if (key in obj) {
956
997
  return get_descriptor(obj, key);
957
998
  }
958
999
  },
959
1000
  ownKeys() {
960
- const obj = get(tracked);
1001
+ /** @type {Record<string | symbol, any> | Record<string | symbol, any>[]} */
1002
+ var obj = fn();
1003
+ /** @type {Record<string | symbol, 1>} */
1004
+ var done = {};
1005
+ /** @type {(string | symbol)[]} */
1006
+ var keys = [];
1007
+
1008
+ // Handle array of objects/spreads
1009
+ if (is_array(obj)) {
1010
+ // Collect all keys from all objects, order doesn't matter
1011
+ /** @type {Record<string | symbol, any>} */
1012
+ var item;
1013
+ for (var i = 0; i < obj.length; i++) {
1014
+ item = obj[i];
1015
+ for (const key of Reflect.ownKeys(item)) {
1016
+ if (done[key]) {
1017
+ continue;
1018
+ }
1019
+ done[key] = 1;
1020
+ keys.push(key);
1021
+ }
1022
+ }
1023
+ return keys;
1024
+ }
1025
+
961
1026
  return Reflect.ownKeys(obj);
962
1027
  },
963
1028
  },
@@ -1,5 +1,6 @@
1
+ import { Readable } from 'stream';
1
2
  /** @import { Component, Derived } from '#server' */
2
- /** @import { render } from 'ripple/server'*/
3
+ /** @import { render, renderToStream, SSRComponent } from 'ripple/server'*/
3
4
  import { DERIVED, UNINITIALIZED } from '../client/constants.js';
4
5
  import { is_tracked_object } from '../client/utils.js';
5
6
  import { escape } from '../../../utils/escaping.js';
@@ -34,12 +35,16 @@ class Output {
34
35
  promises = [];
35
36
  /** @type {Output | null} */
36
37
  #parent = null;
38
+ /** @type {import('stream').Readable | null} */
39
+ #stream = null;
37
40
 
38
41
  /**
39
42
  * @param {Output | null} parent
43
+ * @param {import('stream').Readable | null} stream
40
44
  */
41
- constructor(parent) {
45
+ constructor(parent, stream = null) {
42
46
  this.#parent = parent;
47
+ this.#stream = stream;
43
48
  }
44
49
 
45
50
  /**
@@ -47,7 +52,11 @@ class Output {
47
52
  * @returns {void}
48
53
  */
49
54
  push(str) {
50
- this.body += str;
55
+ if (this.#stream) {
56
+ this.#stream.push(str);
57
+ } else {
58
+ this.body += str;
59
+ }
51
60
  }
52
61
 
53
62
  /**
@@ -61,7 +70,7 @@ class Output {
61
70
 
62
71
  /** @type {render} */
63
72
  export async function render(component) {
64
- const output = new Output(null);
73
+ const output = new Output(null, null);
65
74
  let head = '';
66
75
  let body = '';
67
76
  let css = new Set();
@@ -76,14 +85,45 @@ export async function render(component) {
76
85
  await Promise.all(output.promises);
77
86
  }
78
87
 
79
- head = output.head
80
- body = output.body
81
- css = output.css
88
+ head = output.head;
89
+ body = output.body;
90
+ css = output.css;
91
+ } catch (error) {
92
+ console.log(error);
82
93
  }
83
- catch (error) {
84
- console.log(error)
94
+ return { head, body, css };
95
+ }
96
+
97
+ /** @type {renderToStream} */
98
+ export function renderToStream(component) {
99
+ const stream = new Readable({
100
+ read() {},
101
+ });
102
+ const output = new Output(null, stream);
103
+ render_in_chunks(component, stream, output);
104
+ return stream;
105
+ }
106
+ /**
107
+ *
108
+ * @param {SSRComponent} component
109
+ * @param {Readable} stream
110
+ * @param {Output} output
111
+ */
112
+ async function render_in_chunks(component, stream, output) {
113
+ try {
114
+ if (component.async) {
115
+ await component(output, {});
116
+ } else {
117
+ component(output, {});
118
+ }
119
+ if (output.promises.length > 0) {
120
+ await Promise.all(output.promises);
121
+ }
122
+ stream.push(null);
123
+ } catch (error) {
124
+ console.error(error);
125
+ stream.emit('error', error);
85
126
  }
86
- return { head, body, css }
87
127
  }
88
128
  /**
89
129
  * @returns {void}
@@ -144,7 +184,7 @@ export function get(tracked) {
144
184
  return tracked;
145
185
  }
146
186
 
147
- return (tracked.f & DERIVED) !== 0 ? get_derived(/** @type {Derived} */(tracked)) : tracked.v;
187
+ return (tracked.f & DERIVED) !== 0 ? get_derived(/** @type {Derived} */ (tracked)) : tracked.v;
148
188
  }
149
189
 
150
190
  /**
@@ -1,3 +1,3 @@
1
- export { render } from '../runtime/internal/server/index.js';
1
+ export { render, renderToStream } from '../runtime/internal/server/index.js';
2
2
  export { get_css_for_hashes } from '../runtime/internal/server/css-registry.js';
3
3
  export { executeServerFunction } from '../runtime/internal/server/rpc.js';