ripple 0.2.177 → 0.2.179

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.177",
6
+ "version": "0.2.179",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -80,9 +80,10 @@
80
80
  "@types/estree-jsx": "^1.0.5",
81
81
  "@types/node": "^24.3.0",
82
82
  "typescript": "^5.9.2",
83
- "@volar/language-core": "~2.4.23"
83
+ "@volar/language-core": "~2.4.23",
84
+ "vscode-languageserver-types": "^3.17.5"
84
85
  },
85
86
  "peerDependencies": {
86
- "ripple": "0.2.177"
87
+ "ripple": "0.2.179"
87
88
  }
88
89
  }
@@ -3,6 +3,7 @@ import type {
3
3
  CodeInformation as VolarCodeInformation,
4
4
  Mapping as VolarMapping,
5
5
  } from '@volar/language-core';
6
+ import type { DocumentHighlightKind } from 'vscode-languageserver-types';
6
7
 
7
8
  // ============================================================================
8
9
  // Compiler API Exports
@@ -22,7 +23,28 @@ export interface CompileResult {
22
23
  css: string;
23
24
  }
24
25
 
25
- export interface CustomMappingData {
26
+ export interface PluginActionOverrides {
27
+ /** Whether to enable word document highlighting for this mapping */
28
+ wordHighlight?: {
29
+ kind: DocumentHighlightKind;
30
+ };
31
+ /** TypeScript diagnostic codes to suppress for this mapping */
32
+ suppressedDiagnostics?: number[];
33
+ /** Custom hover documentation for this mapping, false to disable */
34
+ hover?:
35
+ | {
36
+ contents: string;
37
+ }
38
+ | false;
39
+ /** Custom definition info for this mapping, false to disable */
40
+ definition?:
41
+ | {
42
+ description: string;
43
+ }
44
+ | false;
45
+ }
46
+
47
+ export interface CustomMappingData extends PluginActionOverrides {
26
48
  generatedLengths: number[];
27
49
  }
28
50
 
@@ -34,9 +56,6 @@ export interface CodeMapping extends VolarMapping<MappingData> {
34
56
  data: MappingData;
35
57
  }
36
58
 
37
- /**
38
- * Result of Volar mappings compilation
39
- */
40
59
  export interface VolarMappingsResult {
41
60
  code: string;
42
61
  mappings: CodeMapping[];
@@ -47,42 +66,30 @@ export interface VolarMappingsResult {
47
66
  /**
48
67
  * Compilation options
49
68
  */
50
- export interface CompileOptions {
51
- /** Compilation mode: 'client' or 'server' */
69
+
70
+ interface SharedCompileOptions {
71
+ minify_css?: boolean;
72
+ }
73
+ export interface CompileOptions extends SharedCompileOptions {
52
74
  mode?: 'client' | 'server';
53
75
  }
54
76
 
55
77
  export interface ParseOptions {
56
- /** Enable loose mode */
57
78
  loose?: boolean;
58
79
  }
59
80
 
60
- /**
61
- * Parse Ripple source code to ESTree AST
62
- * @param source - The Ripple source code to parse
63
- * @param options - Parse options
64
- * @returns The parsed ESTree Program AST
65
- */
81
+ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode'> {
82
+ to_ts?: boolean;
83
+ }
84
+
85
+ export interface VolarCompileOptions extends ParseOptions, SharedCompileOptions {}
86
+
66
87
  export function parse(source: string, options?: ParseOptions): Program;
67
88
 
68
- /**
69
- * Compile Ripple source code to JS/CSS output
70
- * @param source - The Ripple source code to compile
71
- * @param filename - The filename for source map generation
72
- * @param options - Compilation options (mode: 'client' or 'server')
73
- * @returns The compilation result with AST, JS, and CSS
74
- */
75
89
  export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
76
90
 
77
- /**
78
- * Compile Ripple source to Volar mappings for editor integration
79
- * @param source - The Ripple source code
80
- * @param filename - The filename for source map generation
81
- * @param options - Parse options
82
- * @returns Volar mappings object for editor integration
83
- */
84
91
  export function compile_to_volar_mappings(
85
92
  source: string,
86
93
  filename: string,
87
- options?: ParseOptions,
94
+ options?: VolarCompileOptions,
88
95
  ): VolarMappingsResult;
@@ -1,5 +1,4 @@
1
1
  /** @import { Program } from 'estree' */
2
- /** @import { ParseOptions } from 'ripple/compiler' */
3
2
 
4
3
  import { parse as parse_module } from './phases/1-parse/index.js';
5
4
  import { analyze } from './phases/2-analyze/index.js';
@@ -20,7 +19,7 @@ export function parse(source) {
20
19
  * Compile Ripple source code to JS/CSS output
21
20
  * @param {string} source
22
21
  * @param {string} filename
23
- * @param {{ mode?: 'client' | 'server' }} [options]
22
+ * @param {CompileOptions} [options]
24
23
  * @returns {object}
25
24
  */
26
25
  export function compile(source, filename, options = {}) {
@@ -28,26 +27,32 @@ export function compile(source, filename, options = {}) {
28
27
  const analysis = analyze(ast, filename, options);
29
28
  const result =
30
29
  options.mode === 'server'
31
- ? transform_server(filename, source, analysis)
32
- : transform_client(filename, source, analysis, false);
30
+ ? transform_server(filename, source, analysis, options?.minify_css ?? false)
31
+ : transform_client(filename, source, analysis, false, options?.minify_css ?? false);
33
32
 
34
33
  return result;
35
34
  }
36
35
 
37
36
  /** @import { PostProcessingChanges, LineOffsets } from './phases/3-transform/client/index.js' */
38
- /** @import { VolarMappingsResult } from 'ripple/compiler' */
37
+ /** @import { VolarMappingsResult, VolarCompileOptions, CompileOptions } from 'ripple/compiler' */
39
38
 
40
39
  /**
41
40
  * Compile Ripple component to Volar virtual code with TypeScript mappings
42
41
  * @param {string} source
43
42
  * @param {string} filename
44
- * @param {ParseOptions} [options] - Compiler options
43
+ * @param {VolarCompileOptions} [options] - Compiler options
45
44
  * @returns {VolarMappingsResult} Volar mappings object
46
45
  */
47
46
  export function compile_to_volar_mappings(source, filename, options) {
48
47
  const ast = parse_module(source, options);
49
48
  const analysis = analyze(ast, filename, { to_ts: true, loose: !!options?.loose });
50
- const transformed = transform_client(filename, source, analysis, true);
49
+ const transformed = transform_client(
50
+ filename,
51
+ source,
52
+ analysis,
53
+ true,
54
+ options?.minify_css ?? false,
55
+ );
51
56
 
52
57
  // Create volar mappings with esrap source map for accurate positioning
53
58
  return convert_source_map_to_mappings(
@@ -2115,9 +2115,12 @@ function get_comment_handlers(source, comments, index = 0) {
2115
2115
  }
2116
2116
  // Handle empty Element nodes the same way as empty BlockStatements
2117
2117
  if (node.type === 'Element' && (!node.children || node.children.length === 0)) {
2118
- if (comments[0].start < node.end && comments[0].end < node.end) {
2118
+ // Collect all comments that fall within this empty element
2119
+ while (comments[0] && comments[0].start < node.end && comments[0].end < node.end) {
2119
2120
  comment = /** @type {CommentWithLocation} */ (comments.shift());
2120
2121
  (node.innerComments ||= []).push(comment);
2122
+ }
2123
+ if (node.innerComments && node.innerComments.length > 0) {
2121
2124
  return;
2122
2125
  }
2123
2126
  }
@@ -1,3 +1,5 @@
1
+ /** @import * as AST from 'estree' */
2
+
1
3
  import { hash } from '../../utils.js';
2
4
 
3
5
  const REGEX_MATCHER = /^[~^$*|]?=/;
@@ -17,6 +19,10 @@ const regex_whitespace = /\s/;
17
19
  class Parser {
18
20
  index = 0;
19
21
 
22
+ /**
23
+ * @param {string} template
24
+ * @param {boolean} loose
25
+ */
20
26
  constructor(template, loose) {
21
27
  if (typeof template !== 'string') {
22
28
  throw new TypeError('Template must be a string');
@@ -27,6 +33,7 @@ class Parser {
27
33
  this.template = template.trimEnd();
28
34
  }
29
35
 
36
+ /** @param {string} str */
30
37
  match(str) {
31
38
  const length = str.length;
32
39
  if (length === 1) {
@@ -37,6 +44,11 @@ class Parser {
37
44
  return this.template.slice(this.index, this.index + length) === str;
38
45
  }
39
46
 
47
+ /**
48
+ * @param {string} str
49
+ * @param {boolean} required
50
+ * @param {boolean} required_in_loose
51
+ */
40
52
  eat(str, required = false, required_in_loose = true) {
41
53
  if (this.match(str)) {
42
54
  this.index += str.length;
@@ -50,6 +62,10 @@ class Parser {
50
62
  return false;
51
63
  }
52
64
 
65
+ /**
66
+ * Match a regex at the current index
67
+ * @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
68
+ */
53
69
  match_regex(pattern) {
54
70
  const match = pattern.exec(this.template.slice(this.index));
55
71
  if (!match || match.index !== 0) return null;
@@ -57,6 +73,10 @@ class Parser {
57
73
  return match[0];
58
74
  }
59
75
 
76
+ /**
77
+ * Search for a regex starting at the current index and return the result if it matches
78
+ * @param {RegExp} pattern Should have a ^ anchor at the start so the regex doesn't search past the beginning, resulting in worse performance
79
+ */
60
80
  read(pattern) {
61
81
  const result = this.match_regex(pattern);
62
82
  if (result) this.index += result.length;
@@ -69,6 +89,7 @@ class Parser {
69
89
  }
70
90
  }
71
91
 
92
+ /** @param {RegExp} pattern */
72
93
  read_until(pattern) {
73
94
  if (this.index >= this.template.length) {
74
95
  if (this.loose) return '';
@@ -88,6 +109,11 @@ class Parser {
88
109
  }
89
110
  }
90
111
 
112
+ /**
113
+ * @param {string} content
114
+ * @param {{ loose?: boolean }} options
115
+ * @returns {Partial<AST.CSS.StyleSheet>}
116
+ */
91
117
  export function parse_style(content, options) {
92
118
  const parser = new Parser(content, options.loose || false);
93
119
 
@@ -95,10 +121,11 @@ export function parse_style(content, options) {
95
121
  source: content,
96
122
  hash: `ripple-${hash(content)}`,
97
123
  type: 'StyleSheet',
98
- body: read_body(parser),
124
+ children: read_body(parser),
99
125
  };
100
126
  }
101
127
 
128
+ /** @param {Parser} parser */
102
129
  function allow_comment_or_whitespace(parser) {
103
130
  parser.allow_whitespace();
104
131
  while (parser.match('/*') || parser.match('<!--')) {
@@ -116,7 +143,12 @@ function allow_comment_or_whitespace(parser) {
116
143
  }
117
144
  }
118
145
 
146
+ /**
147
+ * @param {Parser} parser
148
+ * @returns {Array<AST.CSS.Rule | AST.CSS.Atrule>}
149
+ */
119
150
  function read_body(parser) {
151
+ /** @type {Array<AST.CSS.Rule | AST.CSS.Atrule>} */
120
152
  const children = [];
121
153
 
122
154
  while (parser.index < parser.template.length) {
@@ -132,6 +164,10 @@ function read_body(parser) {
132
164
  return children;
133
165
  }
134
166
 
167
+ /**
168
+ * @param {Parser} parser
169
+ * @returns {AST.CSS.Atrule}
170
+ */
135
171
  function read_at_rule(parser) {
136
172
  const start = parser.index;
137
173
  parser.eat('@', true);
@@ -161,6 +197,10 @@ function read_at_rule(parser) {
161
197
  };
162
198
  }
163
199
 
200
+ /**
201
+ * @param {Parser} parser
202
+ * @returns {AST.CSS.Rule}
203
+ */
164
204
  function read_rule(parser) {
165
205
  const start = parser.index;
166
206
 
@@ -178,6 +218,10 @@ function read_rule(parser) {
178
218
  };
179
219
  }
180
220
 
221
+ /**
222
+ * @param {Parser} parser
223
+ * @returns {AST.CSS.Block}
224
+ */
181
225
  function read_block(parser) {
182
226
  const start = parser.index;
183
227
 
@@ -206,6 +250,12 @@ function read_block(parser) {
206
250
  };
207
251
  }
208
252
 
253
+ /**
254
+ * Reads a declaration, rule or at-rule
255
+ *
256
+ * @param {Parser} parser
257
+ * @returns {AST.CSS.Declaration | AST.CSS.Rule | AST.CSS.Atrule}
258
+ */
209
259
  function read_block_item(parser) {
210
260
  if (parser.match('@')) {
211
261
  return read_at_rule(parser);
@@ -221,6 +271,10 @@ function read_block_item(parser) {
221
271
  return char === '{' ? read_rule(parser) : read_declaration(parser);
222
272
  }
223
273
 
274
+ /**
275
+ * @param {Parser} parser
276
+ * @returns {AST.CSS.Declaration}
277
+ */
224
278
  function read_declaration(parser) {
225
279
  const start = parser.index;
226
280
 
@@ -251,6 +305,10 @@ function read_declaration(parser) {
251
305
  };
252
306
  }
253
307
 
308
+ /**
309
+ * @param {Parser} parser
310
+ * @returns {string}
311
+ */
254
312
  function read_value(parser) {
255
313
  let value = '';
256
314
  let escaped = false;
@@ -287,6 +345,11 @@ function read_value(parser) {
287
345
  throw new Error('Unexpected end of input');
288
346
  }
289
347
 
348
+ /**
349
+ * @param {Parser} parser
350
+ * @param {boolean} [inside_pseudo_class]
351
+ * @returns {AST.CSS.SelectorList}
352
+ */
290
353
  function read_selector_list(parser, inside_pseudo_class = false) {
291
354
  /** @type {AST.CSS.ComplexSelector[]} */
292
355
  const children = [];
@@ -318,6 +381,10 @@ function read_selector_list(parser, inside_pseudo_class = false) {
318
381
  throw new Error('Unexpected end of input');
319
382
  }
320
383
 
384
+ /**
385
+ * @param {Parser} parser
386
+ * @returns {AST.CSS.Combinator | null}
387
+ */
321
388
  function read_combinator(parser) {
322
389
  const start = parser.index;
323
390
  parser.allow_whitespace();
@@ -349,6 +416,11 @@ function read_combinator(parser) {
349
416
  return null;
350
417
  }
351
418
 
419
+ /**
420
+ * @param {Parser} parser
421
+ * @param {boolean} [inside_pseudo_class]
422
+ * @returns {AST.CSS.ComplexSelector}
423
+ */
352
424
  function read_selector(parser, inside_pseudo_class = false) {
353
425
  const list_start = parser.index;
354
426
 
@@ -547,7 +619,7 @@ function read_selector(parser, inside_pseudo_class = false) {
547
619
  parser.allow_whitespace();
548
620
 
549
621
  if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
550
- e.css_selector_invalid(parser.index);
622
+ throw new Error(`Invalid selector at parser.index: ${parser.index}`);
551
623
  }
552
624
  }
553
625
  }
@@ -588,6 +660,10 @@ function read_attribute_value(parser) {
588
660
  throw new Error('Unexpected end of input');
589
661
  }
590
662
 
663
+ /**
664
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
665
+ * @param {Parser} parser
666
+ */
591
667
  function read_identifier(parser) {
592
668
  const start = parser.index;
593
669
 
@@ -1,3 +1,6 @@
1
+ /** @import {AnalyzeOptions} from 'ripple/compiler' */
2
+ /** @import * as AST from 'estree' */
3
+
1
4
  import * as b from '../../../utils/builders.js';
2
5
  import { walk } from 'zimmerframe';
3
6
  import { create_scopes, ScopeRoot } from '../../scope.js';
@@ -564,14 +567,15 @@ const visitors = {
564
567
  * @returns
565
568
  */
566
569
  TryStatement(node, context) {
570
+ const { state } = context;
567
571
  if (!is_inside_component(context)) {
568
572
  return context.next();
569
573
  }
570
574
 
571
575
  if (node.pending) {
572
576
  // Try/pending blocks indicate async operations
573
- if (context.state.metadata?.await === false) {
574
- context.state.metadata.await = true;
577
+ if (state.metadata?.await === false) {
578
+ state.metadata.await = true;
575
579
  }
576
580
 
577
581
  node.metadata = {
@@ -579,12 +583,12 @@ const visitors = {
579
583
  has_template: false,
580
584
  };
581
585
 
582
- context.visit(node.block, context.state);
586
+ context.visit(node.block, state);
583
587
 
584
- if (!node.metadata.has_template) {
588
+ if (!node.metadata.has_template && !state.loose) {
585
589
  error(
586
590
  'Component try statements must contain a template in their main body. Move the try statement into an effect if it does not render anything.',
587
- context.state.analysis.module.filename,
591
+ state.analysis.module.filename,
588
592
  node,
589
593
  );
590
594
  }
@@ -594,19 +598,19 @@ const visitors = {
594
598
  has_template: false,
595
599
  };
596
600
 
597
- context.visit(node.pending, context.state);
601
+ context.visit(node.pending, state);
598
602
 
599
- if (!node.metadata.has_template) {
603
+ if (!node.metadata.has_template && !state.loose) {
600
604
  error(
601
605
  'Component try statements must contain a template in their "pending" body. Rendering a pending fallback is required to have a template.',
602
- context.state.analysis.module.filename,
606
+ state.analysis.module.filename,
603
607
  node,
604
608
  );
605
609
  }
606
610
  }
607
611
 
608
612
  if (node.finalizer) {
609
- context.visit(node.finalizer, context.state);
613
+ context.visit(node.finalizer, state);
610
614
  }
611
615
  },
612
616
 
@@ -856,6 +860,12 @@ const visitors = {
856
860
  },
857
861
  };
858
862
 
863
+ /**
864
+ *
865
+ * @param {AST.Program} ast
866
+ * @param {string} filename
867
+ * @param {AnalyzeOptions} options
868
+ */
859
869
  export function analyze(ast, filename, options = {}) {
860
870
  const scope_root = new ScopeRoot();
861
871
 
@@ -2192,6 +2192,18 @@ function transform_ts_child(node, context) {
2192
2192
  catch_handler = b.catch_clause(node.handler.param || null, catch_body);
2193
2193
  }
2194
2194
 
2195
+ let pending_block = null;
2196
+ if (node.pending) {
2197
+ const pending_scope = context.state.scopes.get(node.pending);
2198
+ pending_block = b.try_item_block(
2199
+ transform_body(node.pending.body, {
2200
+ ...context,
2201
+ state: { ...context.state, scope: pending_scope },
2202
+ }),
2203
+ node.pending.loc,
2204
+ );
2205
+ }
2206
+
2195
2207
  let finally_block = null;
2196
2208
  if (node.finalizer) {
2197
2209
  const finally_scope = context.state.scopes.get(node.finalizer);
@@ -2203,7 +2215,7 @@ function transform_ts_child(node, context) {
2203
2215
  );
2204
2216
  }
2205
2217
 
2206
- state.init.push(b.try(try_body, catch_handler, finally_block));
2218
+ state.init.push(b.try(try_body, catch_handler, finally_block, pending_block));
2207
2219
  } else if (node.type === 'Component') {
2208
2220
  const component = visit(node, state);
2209
2221
 
@@ -2809,6 +2821,38 @@ function create_tsx_with_typescript_support() {
2809
2821
  context.visit(node.body);
2810
2822
  }
2811
2823
  },
2824
+ // Custom handler for TryStatement to support Ripple's pending block
2825
+ TryStatement(node, context) {
2826
+ context.write('try ');
2827
+ context.visit(node.block);
2828
+
2829
+ if (node.pending) {
2830
+ // Output the pending block with source mapping for the 'pending' keyword
2831
+ context.write(' ');
2832
+ context.location(
2833
+ node.pending.loc.start.line,
2834
+ node.pending.loc.start.column - 'pending '.length,
2835
+ );
2836
+ context.write('pending ');
2837
+ context.visit(node.pending);
2838
+ }
2839
+
2840
+ if (node.handler) {
2841
+ context.write(' catch');
2842
+ if (node.handler.param) {
2843
+ context.write(' (');
2844
+ context.visit(node.handler.param);
2845
+ context.write(')');
2846
+ }
2847
+ context.write(' ');
2848
+ context.visit(node.handler.body);
2849
+ }
2850
+
2851
+ if (node.finalizer) {
2852
+ context.write(' finally ');
2853
+ context.visit(node.finalizer);
2854
+ }
2855
+ },
2812
2856
  };
2813
2857
  }
2814
2858
 
@@ -2818,9 +2862,10 @@ function create_tsx_with_typescript_support() {
2818
2862
  * @param {string} source - Original source code
2819
2863
  * @param {any} analysis - Analysis result
2820
2864
  * @param {boolean} to_ts - Whether to generate TypeScript output
2865
+ * @param {boolean} minify_css - Whether to minify CSS output
2821
2866
  * @returns {{ ast: any, js: { code: string, map: any, post_processing_changes?: PostProcessingChanges, line_offsets?: LineOffsets }, css: any }}
2822
2867
  */
2823
- export function transform_client(filename, source, analysis, to_ts) {
2868
+ export function transform_client(filename, source, analysis, to_ts, minify_css) {
2824
2869
  /**
2825
2870
  * User's named imports from 'ripple' so we can reuse them in TS output
2826
2871
  * when transforming shorthand syntax. E.g., if the user has already imported
@@ -2966,7 +3011,7 @@ export function transform_client(filename, source, analysis, to_ts) {
2966
3011
  js.line_offsets = line_offsets;
2967
3012
  }
2968
3013
 
2969
- const css = render_stylesheets(state.stylesheets);
3014
+ const css = render_stylesheets(state.stylesheets, minify_css);
2970
3015
 
2971
3016
  return {
2972
3017
  ast: program,
@@ -1,7 +1,4 @@
1
- /**
2
- * @typedef {Object} CustomMappingData
3
- * @property {number[]} generatedLengths
4
- */
1
+ /** @import { CustomMappingData, PluginActionOverrides } from 'ripple/compiler'; */
5
2
 
6
3
  /**
7
4
  * @typedef {import('estree').Position} Position
@@ -19,6 +16,7 @@
19
16
 
20
17
  import { walk } from 'zimmerframe';
21
18
  import { build_source_to_generated_map, get_generated_position } from '../../source-map-utils.js';
19
+ import { DocumentHighlightKind } from 'vscode-languageserver-types';
22
20
 
23
21
  /** @type {VolarCodeMapping['data']} */
24
22
  export const mapping_data = {
@@ -152,6 +150,7 @@ export function convert_source_map_to_mappings(
152
150
  * is_full_import_statement?: boolean,
153
151
  * loc: Location,
154
152
  * end_loc?: Location,
153
+ * metadata?: PluginActionOverrides
155
154
  * }>}
156
155
  */
157
156
  const tokens = [];
@@ -503,10 +502,53 @@ export function convert_source_map_to_mappings(
503
502
  }
504
503
  return;
505
504
  } else if (node.type === 'TryStatement') {
506
- // Visit in source order: block, handler, finalizer
505
+ // Visit in source order: block, pending, handler, finalizer
507
506
  if (node.block) {
508
507
  visit(node.block);
509
508
  }
509
+ if (node.pending) {
510
+ // Add a special token for the 'pending' keyword with customData
511
+ // to suppress TypeScript diagnostics and provide custom hover/definition
512
+ const pendingKeywordLoc = {
513
+ start: {
514
+ line: node.pending.loc.start.line,
515
+ column: node.pending.loc.start.column - 'pending '.length,
516
+ },
517
+ end: {
518
+ line: node.pending.loc.start.line,
519
+ column: node.pending.loc.start.column - 1,
520
+ },
521
+ };
522
+ tokens.push({
523
+ source: 'pending',
524
+ generated: 'pending',
525
+ loc: pendingKeywordLoc,
526
+ metadata: {
527
+ wordHighlight: {
528
+ kind: DocumentHighlightKind.Text,
529
+ },
530
+ suppressedDiagnostics: [
531
+ 1472, // 'catch' or 'finally' expected
532
+ 2304, // Cannot find name 'pending'
533
+ ],
534
+ // suppress all hovers
535
+ hover: false,
536
+
537
+ // Example of a custom hover contents (uses markdown)
538
+ // hover: {
539
+ // contents:
540
+ // '```ripple\npending\n```\n\nRipple-specific keyword for try/pending blocks.\n\nThe `pending` block executes while async operations inside the `try` block are awaiting. This provides a built-in loading state for async components.',
541
+ // },
542
+
543
+ // TODO: Definition is not implemented yet, leaving for future use
544
+ // definition: {
545
+ // description:
546
+ // 'Ripple pending block - executes during async operations in the try block',
547
+ // },
548
+ },
549
+ });
550
+ visit(node.pending);
551
+ }
510
552
  if (node.handler) {
511
553
  visit(node.handler);
512
554
  }
@@ -1333,11 +1375,31 @@ export function convert_source_map_to_mappings(
1333
1375
  );
1334
1376
  gen_start = gen_loc_to_offset(gen_line_col.line, gen_line_col.column);
1335
1377
 
1378
+ /** @type {CustomMappingData} */
1379
+ const customData = {
1380
+ generatedLengths: [gen_length],
1381
+ };
1382
+
1383
+ // Add optional metadata from token if present
1384
+ if (token.metadata) {
1385
+ if ('wordHighlight' in token.metadata) {
1386
+ customData.wordHighlight = token.metadata.wordHighlight;
1387
+ }
1388
+
1389
+ if ('suppressedDiagnostics' in token.metadata) {
1390
+ customData.suppressedDiagnostics = token.metadata.suppressedDiagnostics;
1391
+ }
1392
+ if ('hover' in token.metadata) {
1393
+ customData.hover = token.metadata.hover;
1394
+ }
1395
+ if ('definition' in token.metadata) {
1396
+ customData.definition = token.metadata.definition;
1397
+ }
1398
+ }
1399
+
1336
1400
  data = {
1337
1401
  ...mapping_data,
1338
- customData: {
1339
- generatedLengths: [gen_length],
1340
- },
1402
+ customData,
1341
1403
  };
1342
1404
  }
1343
1405
 
@@ -1,3 +1,5 @@
1
+ /** @import * as ESTree from 'estree' */
2
+
1
3
  import * as b from '../../../../utils/builders.js';
2
4
  import { walk } from 'zimmerframe';
3
5
  import ts from 'esrap/languages/ts';
@@ -804,7 +806,7 @@ const visitors = {
804
806
  },
805
807
  };
806
808
 
807
- export function transform_server(filename, source, analysis) {
809
+ export function transform_server(filename, source, analysis, minify_css) {
808
810
  // Use component metadata collected during the analyze phase
809
811
  const component_metadata = analysis.component_metadata || [];
810
812
 
@@ -823,7 +825,7 @@ export function transform_server(filename, source, analysis) {
823
825
  walk(analysis.ast, { ...state, namespace: 'html' }, visitors)
824
826
  );
825
827
 
826
- const css = render_stylesheets(state.stylesheets);
828
+ const css = render_stylesheets(state.stylesheets, minify_css);
827
829
 
828
830
  // Add CSS registration if there are stylesheets
829
831
  if (state.stylesheets.length > 0 && css) {
@@ -1,25 +1,58 @@
1
+ /** @import * as AST from 'estree' */
2
+ /** @import { Visitors } from 'zimmerframe' */
3
+
4
+ /**
5
+ * @typedef {{
6
+ * code: MagicString;
7
+ * hash: string;
8
+ * minify: boolean;
9
+ * selector: string;
10
+ * keyframes: Record<string, {
11
+ * indexes: number[];
12
+ * local: boolean | undefined;
13
+ * }>;
14
+ * specificity: {
15
+ * bumped: boolean
16
+ * }
17
+ * }} State
18
+ */
19
+
1
20
  import MagicString from 'magic-string';
2
21
  import { walk } from 'zimmerframe';
3
22
 
4
23
  const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
5
24
  const regex_css_name_boundary = /^[\s,;}]$/;
6
25
 
26
+ /** @param {AST.CSS.Atrule} node */
7
27
  const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
8
28
 
29
+ /**
30
+ * @param {string} name
31
+ * @returns {string}
32
+ */
9
33
  function remove_css_prefix(name) {
10
34
  return name.replace(regex_css_browser_prefix, '');
11
35
  }
12
36
 
37
+ /**
38
+ * Walk backwards until we find a non-whitespace character
39
+ * @param {number} end
40
+ * @param {State} state
41
+ */
13
42
  function remove_preceding_whitespace(end, state) {
14
43
  let start = end;
15
44
  while (/\s/.test(state.code.original[start - 1])) start--;
16
45
  if (start < end) state.code.remove(start, end);
17
46
  }
18
47
 
48
+ /** @param {AST.CSS.Rule} rule */
19
49
  function is_used(rule) {
20
50
  return rule.prelude.children.some((selector) => selector.metadata.used);
21
51
  }
22
52
 
53
+ /**
54
+ * @param {Array<AST.CSS.Node>} path
55
+ */
23
56
  function is_in_global_block(path) {
24
57
  return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
25
58
  }
@@ -27,7 +60,7 @@ function is_in_global_block(path) {
27
60
  /**
28
61
  * Check if we're inside a pseudo-class selector that's INSIDE a :global() wrapper
29
62
  * or adjacent to a :global modifier
30
- * @param {any[]} path
63
+ * @param {AST.CSS.Node[]} path
31
64
  */
32
65
  function is_in_global_pseudo(path) {
33
66
  // Walk up the path to find if we're inside a :global() pseudo-class selector with args
@@ -83,6 +116,11 @@ function has_global_in_middle(rule) {
83
116
  return false;
84
117
  }
85
118
 
119
+ /**
120
+ * @param {AST.CSS.PseudoClassSelector} selector
121
+ * @param {AST.CSS.Combinator | null} combinator
122
+ * @param {State} state
123
+ */
86
124
  function remove_global_pseudo_class(selector, combinator, state) {
87
125
  if (selector.args === null) {
88
126
  let start = selector.start;
@@ -98,6 +136,10 @@ function remove_global_pseudo_class(selector, combinator, state) {
98
136
  }
99
137
  }
100
138
 
139
+ /**
140
+ * @param {AST.CSS.Rule} node
141
+ * @param {MagicString} code
142
+ */
101
143
  function escape_comment_close(node, code) {
102
144
  let escaped = false;
103
145
  let in_comment = false;
@@ -121,12 +163,16 @@ function escape_comment_close(node, code) {
121
163
  }
122
164
  }
123
165
 
166
+ /**
167
+ * @param {State} state
168
+ * @param {number} index
169
+ */
124
170
  function append_hash(state, index) {
125
171
  state.code.prependRight(index, `${state.hash}-`);
126
172
  }
127
173
 
128
174
  /**
129
- * @param {AST.CSS.Rule} rule
175
+ * @param {AST.CSS.Rule} rule
130
176
  * @param {boolean} is_in_global_block
131
177
  */
132
178
  function is_empty(rule, is_in_global_block) {
@@ -159,6 +205,7 @@ function is_empty(rule, is_in_global_block) {
159
205
  return true;
160
206
  }
161
207
 
208
+ /** @type {Visitors<AST.CSS.Node, State>} */
162
209
  const visitors = {
163
210
  _: (node, context) => {
164
211
  context.state.code.addSourcemapLocation(node.start);
@@ -189,6 +236,7 @@ const visitors = {
189
236
  const property = node.property && remove_css_prefix(node.property.toLowerCase());
190
237
  if (property === 'animation' || property === 'animation-name') {
191
238
  let index = node.start + node.property.length + 1;
239
+ /** @type {string} */
192
240
  let name = '';
193
241
 
194
242
  while (index < state.code.original.length) {
@@ -368,7 +416,10 @@ const visitors = {
368
416
  ) {
369
417
  // Now check if this pseudo-class is part of a global RelativeSelector
370
418
  for (let j = i - 2; j >= 0; j--) {
371
- if (parentPath[j].type === 'RelativeSelector' && parentPath[j].metadata?.is_global) {
419
+ if (
420
+ parentPath[j].type === 'RelativeSelector' &&
421
+ /** @type {AST.CSS.RelativeSelector} */ (parentPath[j]).metadata?.is_global
422
+ ) {
372
423
  insideScopingPseudo = true;
373
424
  break;
374
425
  }
@@ -462,7 +513,13 @@ const visitors = {
462
513
  },
463
514
  };
464
515
 
465
- export function render_stylesheets(stylesheets) {
516
+ /**
517
+ * Render stylesheets to CSS string
518
+ * @param {AST.CSS.StyleSheet[]} stylesheets
519
+ * @param {boolean} [minify]
520
+ * @returns {string}
521
+ */
522
+ export function render_stylesheets(stylesheets, minify = false) {
466
523
  let css = '';
467
524
 
468
525
  for (const stylesheet of stylesheets) {
@@ -470,6 +527,7 @@ export function render_stylesheets(stylesheets) {
470
527
  const state = {
471
528
  code,
472
529
  hash: stylesheet.hash,
530
+ minify,
473
531
  selector: `.${stylesheet.hash}`,
474
532
  keyframes: {},
475
533
  specificity: {
@@ -1,4 +1,4 @@
1
- import type { SourceLocation } from 'estree';
1
+ import type * as ESTree from 'estree';
2
2
 
3
3
  // Ripple augmentation for ESTree function nodes
4
4
  declare module 'estree' {
@@ -20,6 +20,178 @@ declare module 'estree' {
20
20
  params: Pattern[];
21
21
  body: BlockStatement;
22
22
  }
23
+
24
+ interface TryStatement {
25
+ pending?: BlockStatement | null;
26
+ }
27
+
28
+ export namespace CSS {
29
+ export interface BaseNode {
30
+ start: number;
31
+ end: number;
32
+ }
33
+
34
+ export interface StyleSheet extends BaseNode {
35
+ type: 'StyleSheet';
36
+ children: Array<Atrule | Rule>;
37
+ source: string;
38
+ hash: string;
39
+ }
40
+
41
+ export interface Atrule extends BaseNode {
42
+ type: 'Atrule';
43
+ name: string;
44
+ prelude: string;
45
+ block: Block | null;
46
+ }
47
+
48
+ export interface Rule extends BaseNode {
49
+ type: 'Rule';
50
+ prelude: SelectorList;
51
+ block: Block;
52
+ metadata: {
53
+ parent_rule: Rule | null;
54
+ has_local_selectors: boolean;
55
+ is_global_block: boolean;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * A list of selectors, e.g. `a, b, c {}`
61
+ */
62
+ export interface SelectorList extends BaseNode {
63
+ type: 'SelectorList';
64
+ /**
65
+ * The `a`, `b` and `c` in `a, b, c {}`
66
+ */
67
+ children: ComplexSelector[];
68
+ }
69
+
70
+ /**
71
+ * A complex selector, e.g. `a b c {}`
72
+ */
73
+ export interface ComplexSelector extends BaseNode {
74
+ type: 'ComplexSelector';
75
+ /**
76
+ * The `a`, `b` and `c` in `a b c {}`
77
+ */
78
+ children: RelativeSelector[];
79
+ metadata: {
80
+ rule: Rule | null;
81
+ used: boolean;
82
+ };
83
+ }
84
+
85
+ /**
86
+ * A relative selector, e.g the `a` and `> b` in `a > b {}`
87
+ */
88
+ export interface RelativeSelector extends BaseNode {
89
+ type: 'RelativeSelector';
90
+ /**
91
+ * In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
92
+ */
93
+ combinator: null | Combinator;
94
+ /**
95
+ * The `b:is(...)` in `> b:is(...)`
96
+ */
97
+ selectors: SimpleSelector[];
98
+
99
+ metadata: {
100
+ is_global: boolean;
101
+ is_global_like: boolean;
102
+ scoped: boolean;
103
+ };
104
+ }
105
+
106
+ export interface TypeSelector extends BaseNode {
107
+ type: 'TypeSelector';
108
+ name: string;
109
+ }
110
+
111
+ export interface IdSelector extends BaseNode {
112
+ type: 'IdSelector';
113
+ name: string;
114
+ }
115
+
116
+ export interface ClassSelector extends BaseNode {
117
+ type: 'ClassSelector';
118
+ name: string;
119
+ }
120
+
121
+ export interface AttributeSelector extends BaseNode {
122
+ type: 'AttributeSelector';
123
+ name: string;
124
+ matcher: string | null;
125
+ value: string | null;
126
+ flags: string | null;
127
+ }
128
+
129
+ export interface PseudoElementSelector extends BaseNode {
130
+ type: 'PseudoElementSelector';
131
+ name: string;
132
+ }
133
+
134
+ export interface PseudoClassSelector extends BaseNode {
135
+ type: 'PseudoClassSelector';
136
+ name: string;
137
+ args: SelectorList | null;
138
+ }
139
+
140
+ export interface Percentage extends BaseNode {
141
+ type: 'Percentage';
142
+ value: string;
143
+ }
144
+
145
+ export interface NestingSelector extends BaseNode {
146
+ type: 'NestingSelector';
147
+ name: '&';
148
+ }
149
+
150
+ export interface Nth extends BaseNode {
151
+ type: 'Nth';
152
+ value: string;
153
+ }
154
+
155
+ export type SimpleSelector =
156
+ | TypeSelector
157
+ | IdSelector
158
+ | ClassSelector
159
+ | AttributeSelector
160
+ | PseudoElementSelector
161
+ | PseudoClassSelector
162
+ | Percentage
163
+ | Nth
164
+ | NestingSelector;
165
+
166
+ export interface Combinator extends BaseNode {
167
+ type: 'Combinator';
168
+ name: string;
169
+ }
170
+
171
+ export interface Block extends BaseNode {
172
+ type: 'Block';
173
+ children: Array<Declaration | Rule | Atrule>;
174
+ }
175
+
176
+ export interface Declaration extends BaseNode {
177
+ type: 'Declaration';
178
+ property: string;
179
+ value: string;
180
+ }
181
+
182
+ // for zimmerframe
183
+ export type Node =
184
+ | StyleSheet
185
+ | Rule
186
+ | Atrule
187
+ | SelectorList
188
+ | Block
189
+ | ComplexSelector
190
+ | RelativeSelector
191
+ | Combinator
192
+ | SimpleSelector
193
+ | Declaration;
194
+ }
23
195
  }
24
196
 
25
197
  declare module 'estree-jsx' {
@@ -28,11 +200,11 @@ declare module 'estree-jsx' {
28
200
  }
29
201
 
30
202
  interface JSXOpeningElement {
31
- loc: SourceLocation;
203
+ loc: ESTree.SourceLocation;
32
204
  }
33
205
 
34
206
  interface JSXClosingElement {
35
- loc: SourceLocation;
207
+ loc: ESTree.SourceLocation;
36
208
  }
37
209
  }
38
210
 
@@ -1,7 +1,7 @@
1
1
  /** @import { Block } from '#client' */
2
2
 
3
3
  import { branch, destroy_block, render, render_spread } from './blocks.js';
4
- import { COMPOSITE_BLOCK, NAMESPACE_URI, DEFAULT_NAMESPACE } from './constants.js';
4
+ import { COMPOSITE_BLOCK, DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
5
5
  import { active_block, active_namespace, with_ns } from './runtime.js';
6
6
  import { top_element_to_ns } from './utils.js';
7
7
 
@@ -32,8 +32,8 @@ export function composite(get_component, node, props) {
32
32
  var block = active_block;
33
33
  /** @type {ComponentFunction} */ (component)(anchor, props, block);
34
34
  });
35
- } else {
36
- // Custom element
35
+ } else if (component != null) {
36
+ // Custom element - only create if component is not null/undefined
37
37
  var run = () => {
38
38
  var block = /** @type {Block} */ (active_block);
39
39
 
@@ -106,6 +106,15 @@ export function block(body) {
106
106
  return { type: 'BlockStatement', body };
107
107
  }
108
108
 
109
+ /**
110
+ * @param {ESTree.Statement[]} body
111
+ * @param {ESTree.SourceLocation} loc
112
+ * @returns {ESTree.BlockStatement}
113
+ */
114
+ export function try_item_block(body, loc) {
115
+ return { type: 'BlockStatement', body, loc };
116
+ }
117
+
109
118
  /**
110
119
  * @param {string} name
111
120
  * @param {ESTree.Statement} body
@@ -681,21 +690,23 @@ export function throw_error(str) {
681
690
  * @param {ESTree.BlockStatement} block
682
691
  * @param {ESTree.CatchClause | null} handler
683
692
  * @param {ESTree.BlockStatement | null} finalizer
693
+ * @param {ESTree.BlockStatement | null} pending
684
694
  * @returns {ESTree.TryStatement}
685
695
  */
686
- export function try_builder(block, handler = null, finalizer = null) {
696
+ export function try_builder(block, handler = null, finalizer = null, pending = null) {
687
697
  return {
688
698
  type: 'TryStatement',
689
699
  block,
690
700
  handler,
691
701
  finalizer,
702
+ pending,
692
703
  };
693
704
  }
694
705
 
695
706
  /**
696
707
  * @param {ESTree.Pattern | null} param
697
708
  * @param {ESTree.BlockStatement} body
698
- * @returns {ESTree.CatchClause}
709
+ * @return {ESTree.CatchClause}
699
710
  */
700
711
  export function catch_clause_builder(param, body) {
701
712
  return {
@@ -59,7 +59,6 @@ describe('composite > render', () => {
59
59
  let name = 'Click Me';
60
60
 
61
61
  <Child class="my-button">{name}</Child>
62
-
63
62
  }
64
63
 
65
64
  component Child({ children, ...rest }: { children: string; class: string }) {
@@ -82,4 +81,25 @@ describe('composite > render', () => {
82
81
 
83
82
  render(ArrayTest);
84
83
  });
84
+
85
+ it('should not render <undefined> tag when a passed in component is undefined', () => {
86
+ component Child({ children, NonExistent, ...props }) {
87
+ <div {...props}>
88
+ <children />
89
+ <NonExistent />
90
+ </div>
91
+ }
92
+
93
+ component App() {
94
+ <Child />
95
+ }
96
+
97
+ render(App);
98
+
99
+ const div = container.querySelector('div');
100
+ const undefinedTag = container.querySelector('undefined');
101
+
102
+ expect(undefinedTag).toBeNull();
103
+ expect(div.innerHTML).not.toContain('<undefined');
104
+ });
85
105
  });