ripple 0.2.103 → 0.2.105

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.
@@ -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,
@@ -65,6 +65,11 @@ export interface TrackedArrayExpression extends Omit<ArrayExpression, 'type'> {
65
65
  elements: (Expression | null)[];
66
66
  }
67
67
 
68
+ export interface TrackedExpression extends Omit<Expression, 'type'> {
69
+ argument: Expression;
70
+ type: 'TrackedExpression';
71
+ }
72
+
68
73
  /**
69
74
  * Tracked object expression node
70
75
  */
@@ -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,70 @@
1
+ import { DEV } from 'esm-env';
2
+
3
+ export function remove_ssr_css() {
4
+ if (!document || typeof requestAnimationFrame !== "function") {
5
+ return;
6
+ }
7
+
8
+ remove_styles();
9
+ }
10
+
11
+ function remove_styles() {
12
+ if (DEV) {
13
+ const styles = document.querySelector('style[data-vite-dev-id]');
14
+ if (styles) {
15
+ remove();
16
+ } else {
17
+ requestAnimationFrame(remove_styles);
18
+ }
19
+ } else {
20
+ remove_when_css_loaded(() => requestAnimationFrame(remove));
21
+ }
22
+ }
23
+
24
+ function remove() {
25
+ document.querySelectorAll("style[data-ripple-ssr]").forEach((el) => el.remove());
26
+ }
27
+
28
+ /**
29
+ * @param {function} callback
30
+ * @returns {void}
31
+ */
32
+ function remove_when_css_loaded(callback) {
33
+ /** @type {HTMLLinkElement[]} */
34
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
35
+ let remaining = links.length;
36
+
37
+ if (remaining === 0) {
38
+ callback();
39
+ return;
40
+ }
41
+
42
+ const done = () => {
43
+ remaining--;
44
+ if (remaining === 0) {
45
+ // clean up all listeners
46
+ links.forEach((link) => {
47
+ link.removeEventListener('load', onLoad);
48
+ link.removeEventListener('error', onError);
49
+ });
50
+ callback();
51
+ }
52
+ };
53
+
54
+ function onLoad() {
55
+ done();
56
+ }
57
+ function onError() {
58
+ done();
59
+ }
60
+
61
+ links.forEach((link) => {
62
+ if (link.sheet) {
63
+ // already loaded (possibly cached)
64
+ done();
65
+ } else {
66
+ link.addEventListener('load', onLoad);
67
+ link.addEventListener('error', onError);
68
+ }
69
+ });
70
+ }
@@ -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';
@@ -0,0 +1,34 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`TrackedExpression tests > should handle the syntax correctly 1`] = `
4
+ <div>
5
+ <div>
6
+ 0
7
+ </div>
8
+ <div>
9
+ 0
10
+ </div>
11
+ <div>
12
+ 1
13
+ </div>
14
+ <div>
15
+ 2
16
+ </div>
17
+ <div>
18
+ 2
19
+ </div>
20
+ <div>
21
+ 3
22
+ </div>
23
+ <div>
24
+ 4
25
+ </div>
26
+ <div>
27
+ false
28
+ </div>
29
+ <div>
30
+ true
31
+ </div>
32
+
33
+ </div>
34
+ `;