ripple 0.2.178 → 0.2.180

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.178",
6
+ "version": "0.2.180",
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.178"
87
+ "ripple": "0.2.180"
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
@@ -23,6 +24,10 @@ export interface CompileResult {
23
24
  }
24
25
 
25
26
  export interface PluginActionOverrides {
27
+ /** Whether to enable word document highlighting for this mapping */
28
+ wordHighlight?: {
29
+ kind: DocumentHighlightKind;
30
+ };
26
31
  /** TypeScript diagnostic codes to suppress for this mapping */
27
32
  suppressedDiagnostics?: number[];
28
33
  /** Custom hover documentation for this mapping, false to disable */
@@ -51,9 +56,6 @@ export interface CodeMapping extends VolarMapping<MappingData> {
51
56
  data: MappingData;
52
57
  }
53
58
 
54
- /**
55
- * Result of Volar mappings compilation
56
- */
57
59
  export interface VolarMappingsResult {
58
60
  code: string;
59
61
  mappings: CodeMapping[];
@@ -64,42 +66,30 @@ export interface VolarMappingsResult {
64
66
  /**
65
67
  * Compilation options
66
68
  */
67
- export interface CompileOptions {
68
- /** Compilation mode: 'client' or 'server' */
69
+
70
+ interface SharedCompileOptions {
71
+ minify_css?: boolean;
72
+ }
73
+ export interface CompileOptions extends SharedCompileOptions {
69
74
  mode?: 'client' | 'server';
70
75
  }
71
76
 
72
77
  export interface ParseOptions {
73
- /** Enable loose mode */
74
78
  loose?: boolean;
75
79
  }
76
80
 
77
- /**
78
- * Parse Ripple source code to ESTree AST
79
- * @param source - The Ripple source code to parse
80
- * @param options - Parse options
81
- * @returns The parsed ESTree Program AST
82
- */
81
+ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode'> {
82
+ to_ts?: boolean;
83
+ }
84
+
85
+ export interface VolarCompileOptions extends ParseOptions, SharedCompileOptions {}
86
+
83
87
  export function parse(source: string, options?: ParseOptions): Program;
84
88
 
85
- /**
86
- * Compile Ripple source code to JS/CSS output
87
- * @param source - The Ripple source code to compile
88
- * @param filename - The filename for source map generation
89
- * @param options - Compilation options (mode: 'client' or 'server')
90
- * @returns The compilation result with AST, JS, and CSS
91
- */
92
89
  export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
93
90
 
94
- /**
95
- * Compile Ripple source to Volar mappings for editor integration
96
- * @param source - The Ripple source code
97
- * @param filename - The filename for source map generation
98
- * @param options - Parse options
99
- * @returns Volar mappings object for editor integration
100
- */
101
91
  export function compile_to_volar_mappings(
102
92
  source: string,
103
93
  filename: string,
104
- options?: ParseOptions,
94
+ options?: VolarCompileOptions,
105
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
 
@@ -2862,9 +2862,10 @@ function create_tsx_with_typescript_support() {
2862
2862
  * @param {string} source - Original source code
2863
2863
  * @param {any} analysis - Analysis result
2864
2864
  * @param {boolean} to_ts - Whether to generate TypeScript output
2865
+ * @param {boolean} minify_css - Whether to minify CSS output
2865
2866
  * @returns {{ ast: any, js: { code: string, map: any, post_processing_changes?: PostProcessingChanges, line_offsets?: LineOffsets }, css: any }}
2866
2867
  */
2867
- export function transform_client(filename, source, analysis, to_ts) {
2868
+ export function transform_client(filename, source, analysis, to_ts, minify_css) {
2868
2869
  /**
2869
2870
  * User's named imports from 'ripple' so we can reuse them in TS output
2870
2871
  * when transforming shorthand syntax. E.g., if the user has already imported
@@ -3010,7 +3011,7 @@ export function transform_client(filename, source, analysis, to_ts) {
3010
3011
  js.line_offsets = line_offsets;
3011
3012
  }
3012
3013
 
3013
- const css = render_stylesheets(state.stylesheets);
3014
+ const css = render_stylesheets(state.stylesheets, minify_css);
3014
3015
 
3015
3016
  return {
3016
3017
  ast: program,
@@ -1,4 +1,5 @@
1
1
  /** @import { CustomMappingData, PluginActionOverrides } from 'ripple/compiler'; */
2
+ /** @import { DocumentHighlightKind } from 'vscode-languageserver-types'; */
2
3
 
3
4
  /**
4
5
  * @typedef {import('estree').Position} Position
@@ -523,6 +524,10 @@ export function convert_source_map_to_mappings(
523
524
  generated: 'pending',
524
525
  loc: pendingKeywordLoc,
525
526
  metadata: {
527
+ wordHighlight: {
528
+ /** @type {DocumentHighlightKind} */
529
+ kind: 1,
530
+ },
526
531
  suppressedDiagnostics: [
527
532
  1472, // 'catch' or 'finally' expected
528
533
  2304, // Cannot find name 'pending'
@@ -1378,6 +1383,10 @@ export function convert_source_map_to_mappings(
1378
1383
 
1379
1384
  // Add optional metadata from token if present
1380
1385
  if (token.metadata) {
1386
+ if ('wordHighlight' in token.metadata) {
1387
+ customData.wordHighlight = token.metadata.wordHighlight;
1388
+ }
1389
+
1381
1390
  if ('suppressedDiagnostics' in token.metadata) {
1382
1391
  customData.suppressedDiagnostics = token.metadata.suppressedDiagnostics;
1383
1392
  }
@@ -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' {
@@ -24,6 +24,174 @@ declare module 'estree' {
24
24
  interface TryStatement {
25
25
  pending?: BlockStatement | null;
26
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
+ }
27
195
  }
28
196
 
29
197
  declare module 'estree-jsx' {
@@ -32,11 +200,11 @@ declare module 'estree-jsx' {
32
200
  }
33
201
 
34
202
  interface JSXOpeningElement {
35
- loc: SourceLocation;
203
+ loc: ESTree.SourceLocation;
36
204
  }
37
205
 
38
206
  interface JSXClosingElement {
39
- loc: SourceLocation;
207
+ loc: ESTree.SourceLocation;
40
208
  }
41
209
  }
42
210
 
@@ -80,9 +80,10 @@ export function render_spread(element, fn, flags = 0) {
80
80
  /**
81
81
  * @param {Function} fn
82
82
  * @param {number} [flags]
83
+ * @param {any} [state]
83
84
  */
84
- export function branch(fn, flags = 0) {
85
- return block(BRANCH_BLOCK | flags, fn);
85
+ export function branch(fn, flags = 0, state = null) {
86
+ return block(BRANCH_BLOCK | flags, fn, state);
86
87
  }
87
88
 
88
89
  /**
@@ -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
 
@@ -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
  });