ripple 0.2.188 → 0.2.190

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.188",
6
+ "version": "0.2.190",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -38,6 +38,12 @@
38
38
  "require": "./src/compiler/index.js",
39
39
  "default": "./src/compiler/index.js"
40
40
  },
41
+ "./compiler/internal/import": {
42
+ "types": "./src/compiler/types/import.d.ts"
43
+ },
44
+ "./compiler/internal/identifier/utils": {
45
+ "default": "./src/compiler/identifier-utils.js"
46
+ },
41
47
  "./validator": {
42
48
  "types": "./types/index.d.ts",
43
49
  "require": "./validator/index.js",
@@ -86,6 +92,6 @@
86
92
  "vscode-languageserver-types": "^3.17.5"
87
93
  },
88
94
  "peerDependencies": {
89
- "ripple": "0.2.188"
95
+ "ripple": "0.2.190"
90
96
  }
91
97
  }
@@ -0,0 +1,75 @@
1
+ export const IDENTIFIER_OBFUSCATION_PREFIX = '_$_';
2
+ export const STYLE_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'style';
3
+ export const SERVER_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + encode_utf16_char('#') + 'server';
4
+ export const CSS_HASH_IDENTIFIER = IDENTIFIER_OBFUSCATION_PREFIX + 'hash';
5
+
6
+ const DECODE_UTF16_REGEX = /_u([0-9a-fA-F]{4})_/g;
7
+
8
+ /**
9
+ * @param {string} char
10
+ * @returns {string}
11
+ */
12
+ function encode_utf16_char(char) {
13
+ return `_u${('0000' + char.charCodeAt(0).toString(16)).slice(-4)}_`;
14
+ }
15
+
16
+ /**
17
+ * @param {string} encoded
18
+ * @returns {string}
19
+ */
20
+ function decoded_utf16_string(encoded) {
21
+ return encoded.replace(DECODE_UTF16_REGEX, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
22
+ }
23
+
24
+ /**
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ export function obfuscate_identifier(name) {
29
+ let start = 0;
30
+ if (name[0] === name[0].toUpperCase()) {
31
+ start = 1;
32
+ }
33
+ const index = find_next_uppercase(name, start);
34
+
35
+ const first_part = name.slice(0, index);
36
+ const second_part = name.slice(index);
37
+
38
+ return (
39
+ IDENTIFIER_OBFUSCATION_PREFIX +
40
+ (second_part ? second_part + '__' + first_part : first_part + '__')
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @param {string} name
46
+ * @returns {boolean}
47
+ */
48
+ export function is_identifier_obfuscated(name) {
49
+ return name.startsWith(IDENTIFIER_OBFUSCATION_PREFIX);
50
+ }
51
+
52
+ /**
53
+ * @param {string} name
54
+ * @returns {string}
55
+ */
56
+ export function deobfuscate_identifier(name) {
57
+ name = name.replaceAll(IDENTIFIER_OBFUSCATION_PREFIX, '');
58
+ const parts = name.split('__');
59
+ return decoded_utf16_string((parts[1] ? parts[1] : '') + parts[0]);
60
+ }
61
+
62
+ /**
63
+ * Finds the next uppercase character or returns name.length
64
+ * @param {string} name
65
+ * @param {number} start
66
+ * @returns {number}
67
+ */
68
+ function find_next_uppercase(name, start) {
69
+ for (let i = start; i < name.length; i++) {
70
+ if (name[i] === name[i].toUpperCase()) {
71
+ return i;
72
+ }
73
+ }
74
+ return name.length;
75
+ }
@@ -1,4 +1,4 @@
1
- import type { Program } from 'estree';
1
+ import type * as AST from 'estree';
2
2
  import type {
3
3
  CodeInformation as VolarCodeInformation,
4
4
  Mapping as VolarMapping,
@@ -14,7 +14,7 @@ import type { SourceMapMappings } from '@jridgewell/sourcemap-codec';
14
14
  */
15
15
  export interface CompileResult {
16
16
  /** The transformed AST */
17
- ast: Program;
17
+ ast: AST.Program;
18
18
  /** The generated JavaScript code with source map */
19
19
  js: {
20
20
  code: string;
@@ -49,6 +49,14 @@ export interface PluginActionOverrides {
49
49
  description?: string; // just for reference
50
50
  // Generic location for embedded content (CSS, etc.)
51
51
  location?: DefinitionLocation;
52
+ // Replace the type name in hover/definition with a different name
53
+ // And provide the path to import the type definitions from
54
+ // the `ripple` package directory, e.g. `types/index.d.ts`
55
+ // Currently only supported by the definition plugin
56
+ typeReplace?: {
57
+ name: string;
58
+ path: string;
59
+ };
52
60
  }
53
61
  | false;
54
62
  }
@@ -94,7 +102,7 @@ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode
94
102
 
95
103
  export interface VolarCompileOptions extends ParseOptions, SharedCompileOptions {}
96
104
 
97
- export function parse(source: string, options?: ParseOptions): Program;
105
+ export function parse(source: string, options?: ParseOptions): AST.Program;
98
106
 
99
107
  export function compile(source: string, filename: string, options?: CompileOptions): CompileResult;
100
108
 
@@ -1,4 +1,4 @@
1
- /** @import { Program } from 'estree' */
1
+ /** @import * as AST from 'estree' */
2
2
 
3
3
  import { parse as parse_module } from './phases/1-parse/index.js';
4
4
  import { analyze } from './phases/2-analyze/index.js';
@@ -9,7 +9,7 @@ import { convert_source_map_to_mappings } from './phases/3-transform/segments.js
9
9
  /**
10
10
  * Parse Ripple source code to ESTree AST
11
11
  * @param {string} source
12
- * @returns {Program}
12
+ * @returns {AST.Program}
13
13
  */
14
14
  export function parse(source) {
15
15
  return parse_module(source, undefined);
@@ -342,8 +342,12 @@ function RipplePlugin(config) {
342
342
  if (this.input.slice(this.pos, this.pos + 4) === '#Map') {
343
343
  const charAfter =
344
344
  this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
345
- if (charAfter === 40) {
346
- // ( character
345
+ if (charAfter === 40 || charAfter === 60) {
346
+ // ( or < character (for generics like #Map<string, number>)
347
+ this.pos += 4; // consume '#Map'
348
+ return this.finishToken(tt.name, '#Map');
349
+ } else if (this.#loose) {
350
+ // In loose mode, produce token even without parens (incomplete syntax)
347
351
  this.pos += 4; // consume '#Map'
348
352
  return this.finishToken(tt.name, '#Map');
349
353
  }
@@ -351,8 +355,12 @@ function RipplePlugin(config) {
351
355
  if (this.input.slice(this.pos, this.pos + 4) === '#Set') {
352
356
  const charAfter =
353
357
  this.pos + 4 < this.input.length ? this.input.charCodeAt(this.pos + 4) : -1;
354
- if (charAfter === 40) {
355
- // ( character
358
+ if (charAfter === 40 || charAfter === 60) {
359
+ // ( or < character (for generics like #Set<number>)
360
+ this.pos += 4; // consume '#Set'
361
+ return this.finishToken(tt.name, '#Set');
362
+ } else if (this.#loose) {
363
+ // In loose mode, produce token even without parens (incomplete syntax)
356
364
  this.pos += 4; // consume '#Set'
357
365
  return this.finishToken(tt.name, '#Set');
358
366
  }
@@ -378,9 +386,29 @@ function RipplePlugin(config) {
378
386
  }
379
387
  }
380
388
 
389
+ if (this.input.slice(this.pos, this.pos + 6) === '#style') {
390
+ // Check that next char after 'style' is . (dot), [, whitespace, or EOF
391
+ const charAfter =
392
+ this.pos + 6 < this.input.length ? this.input.charCodeAt(this.pos + 6) : -1;
393
+ if (
394
+ charAfter === 46 || // . (dot)
395
+ charAfter === 91 || // [
396
+ charAfter === 32 || // space
397
+ charAfter === 9 || // tab
398
+ charAfter === 10 || // newline
399
+ charAfter === 13 || // carriage return
400
+ charAfter === -1 // EOF
401
+ ) {
402
+ // { or . or whitespace or EOF
403
+ this.pos += 6; // consume '#style'
404
+ return this.finishToken(tt.name, '#style');
405
+ }
406
+ }
407
+
381
408
  // Check if this is an invalid #Identifier pattern
382
- // Valid patterns: #[, #{, #Map(, #Set(, #server
409
+ // Valid patterns: #[, #{, #Map(, #Map<, #Set(, #Set<, #server, #style
383
410
  // If we see # followed by an uppercase letter that isn't Map or Set, it's an error
411
+ // In loose mode, allow incomplete identifiers like #M, #Ma, #S, #Se for autocomplete
384
412
  if (nextChar >= 65 && nextChar <= 90) {
385
413
  // A-Z
386
414
  // Extract the identifier name
@@ -401,12 +429,34 @@ function RipplePlugin(config) {
401
429
  }
402
430
  const identName = this.input.slice(this.pos + 1, identEnd);
403
431
  if (identName !== 'Map' && identName !== 'Set') {
404
- this.raise(
405
- this.pos,
406
- `Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
407
- );
432
+ // In loose mode, allow incomplete identifiers (prefixes of Map/Set)
433
+ // This supports autocomplete scenarios where user is still typing
434
+ const isIncompleteMap = 'Map'.startsWith(identName);
435
+ const isIncompleteSet = 'Set'.startsWith(identName);
436
+
437
+ if (!this.#loose || (!isIncompleteMap && !isIncompleteSet)) {
438
+ this.raise(
439
+ this.pos,
440
+ `Invalid tracked syntax '#${identName}'. Only #Map and #Set are currently supported using shorthand tracked syntax.`,
441
+ );
442
+ } else {
443
+ // In loose mode with valid prefix, consume the token and return it
444
+ // This allows the parser to handle incomplete syntax gracefully
445
+ this.pos = identEnd; // consume '#' + identifier
446
+ return this.finishToken(tt.name, '#' + identName);
447
+ }
408
448
  }
409
449
  }
450
+
451
+ // In loose mode, handle bare # or # followed by unrecognized characters
452
+ if (this.#loose) {
453
+ this.pos++; // consume '#'
454
+ return this.finishToken(tt.name, '#');
455
+ }
456
+ } else if (this.#loose) {
457
+ // In loose mode, handle bare # at EOF
458
+ this.pos++; // consume '#'
459
+ return this.finishToken(tt.name, '#');
410
460
  }
411
461
  }
412
462
  if (code === 64) {
@@ -435,20 +485,43 @@ function RipplePlugin(config) {
435
485
  // In JSX expressions, inside parentheses, assignments, etc.
436
486
  // we want to treat @ as an identifier prefix rather than decorator
437
487
  const currentType = this.type;
438
- const inExpression =
439
- this.exprAllowed ||
440
- currentType === tt.braceL || // Inside { }
441
- currentType === tt.parenL || // Inside ( )
442
- currentType === tt.eq || // After =
443
- currentType === tt.comma || // After ,
444
- currentType === tt.colon || // After :
445
- currentType === tt.question || // After ?
446
- currentType === tt.logicalOR || // After ||
447
- currentType === tt.logicalAND || // After &&
448
- currentType === tt.dot || // After . (for member expressions like obj.@prop)
449
- currentType === tt.questionDot; // After ?. (for optional chaining like obj?.@prop)
450
-
451
- if (inExpression) {
488
+ /**
489
+ * @param {Parse.TokenType} type
490
+ * @param {Parse.Parser} parser
491
+ * @param {Parse.TokTypes} tt
492
+ * @returns {boolean}
493
+ */
494
+ function inExpression(type, parser, tt) {
495
+ return (
496
+ parser.exprAllowed ||
497
+ type === tt.braceL || // Inside { }
498
+ type === tt.parenL || // Inside ( )
499
+ type === tt.eq || // After =
500
+ type === tt.comma || // After ,
501
+ type === tt.colon || // After :
502
+ type === tt.question || // After ?
503
+ type === tt.logicalOR || // After ||
504
+ type === tt.logicalAND || // After &&
505
+ type === tt.dot || // After . (for member expressions like obj.@prop)
506
+ type === tt.questionDot // After ?. (for optional chaining like obj?.@prop)
507
+ );
508
+ }
509
+
510
+ /**
511
+ * @param {Parse.Parser} parser
512
+ * @param {Parse.TokTypes} tt
513
+ * @returns {boolean}
514
+ */
515
+ function inAwait(parser, tt) {
516
+ return currentType === tt.name &&
517
+ parser.value === 'await' &&
518
+ parser.canAwait &&
519
+ parser.preToken
520
+ ? inExpression(parser.preToken, parser, tt)
521
+ : false;
522
+ }
523
+
524
+ if (inExpression(currentType, this, tt) || inAwait(this, tt)) {
452
525
  return this.readAtIdentifier();
453
526
  }
454
527
  }
@@ -604,12 +677,32 @@ function RipplePlugin(config) {
604
677
  return /** @type {AST.ServerIdentifier} */ (this.finishNode(node, 'ServerIdentifier'));
605
678
  }
606
679
 
680
+ if (this.type === tt.name && this.value === '#style') {
681
+ const node = this.startNode();
682
+ this.next();
683
+ return /** @type {AST.StyleIdentifier} */ (this.finishNode(node, 'StyleIdentifier'));
684
+ }
685
+
607
686
  // Check if this is #Map( or #Set(
608
687
  if (this.type === tt.name && (this.value === '#Map' || this.value === '#Set')) {
609
688
  const type = this.value === '#Map' ? 'TrackedMapExpression' : 'TrackedSetExpression';
610
689
  return this.parseTrackedCollectionExpression(type);
611
690
  }
612
691
 
692
+ // In loose mode, handle incomplete #Map/#Set prefixes (e.g., #M, #Ma, #S, #Se)
693
+ if (
694
+ this.#loose &&
695
+ this.type === tt.name &&
696
+ typeof this.value === 'string' &&
697
+ this.value.startsWith('#')
698
+ ) {
699
+ // Return an Identifier node for incomplete tracked syntax
700
+ const node = /** @type {AST.Identifier} */ (this.startNode());
701
+ node.name = this.value;
702
+ this.next();
703
+ return this.finishNode(node, 'Identifier');
704
+ }
705
+
613
706
  // Check if this is a tuple literal starting with #[
614
707
  if (this.type === tt.bracketL && this.value === '#[') {
615
708
  return this.parseTrackedArrayExpression();
@@ -676,7 +769,7 @@ function RipplePlugin(config) {
676
769
  const node = /** @type {AST.ServerBlock} */ (this.startNode());
677
770
  this.next();
678
771
 
679
- const body = /** @type {AST.BlockStatement} */ (this.startNode());
772
+ const body = /** @type {AST.ServerBlockStatement} */ (this.startNode());
680
773
  node.body = body;
681
774
  body.body = [];
682
775
 
@@ -728,8 +821,10 @@ function RipplePlugin(config) {
728
821
  );
729
822
  }
730
823
 
731
- if (this.type === tt.parenL) {
732
- // Don't consume parens - they belong to NewExpression
824
+ // Don't consume parens or generics - they belong to NewExpression
825
+ // When used as "new #Map(...)" the next token is '('
826
+ // When used as "new #Map<K,V>(...)" the next token is '<' (relational)
827
+ if (this.type === tt.parenL || (this.type === tt.relational && this.value === '<')) {
733
828
  node.arguments = [];
734
829
  return this.finishNode(node, type);
735
830
  }
@@ -6,6 +6,8 @@
6
6
  AnalysisContext,
7
7
  ScopeInterface,
8
8
  Visitors,
9
+ TopScopedClasses,
10
+ StyleClasses,
9
11
  } from '#compiler';
10
12
  */
11
13
  /** @import * as AST from 'estree' */
@@ -200,6 +202,51 @@ const visitors = {
200
202
  context.state.metadata.tracking = true;
201
203
  }
202
204
 
205
+ // Track #style.className or #style['className'] references
206
+ if (node.object.type === 'StyleIdentifier') {
207
+ const component = is_inside_component(context, true);
208
+
209
+ if (!component) {
210
+ return error(
211
+ '`#style` can only be used within a component',
212
+ context.state.analysis.module.filename,
213
+ node,
214
+ );
215
+ } else {
216
+ component.metadata.styleIdentifierPresent = true;
217
+ }
218
+
219
+ /** @type {string | null} */
220
+ let className = null;
221
+
222
+ if (!node.computed && node.property.type === 'Identifier') {
223
+ // #style.test
224
+ className = node.property.name;
225
+ } else if (
226
+ node.computed &&
227
+ node.property.type === 'Literal' &&
228
+ typeof node.property.value === 'string'
229
+ ) {
230
+ // #style['test']
231
+ className = node.property.value;
232
+ } else {
233
+ // #style[expression] - dynamic, not allowed
234
+ error(
235
+ '`#style` property access must use a dot property or static string for css class name, not a dynamic expression',
236
+ context.state.analysis.module.filename,
237
+ node.property,
238
+ );
239
+ }
240
+
241
+ if (className !== null) {
242
+ context.state.metadata.styleClasses?.set(className, node.property);
243
+ }
244
+
245
+ return context.next();
246
+ } else if (node.object.type === 'ServerIdentifier') {
247
+ context.state.analysis.metadata.serverIdentifierPresent = true;
248
+ }
249
+
203
250
  if (node.object.type === 'Identifier' && !node.object.tracked) {
204
251
  const binding = context.state.scope.get(node.object.name);
205
252
 
@@ -372,7 +419,13 @@ const visitors = {
372
419
  const elements = [];
373
420
 
374
421
  // Track metadata for this component
375
- const metadata = { await: false };
422
+ const metadata = {
423
+ await: false,
424
+ styleClasses: /** @type {StyleClasses} */ (new Map()),
425
+ };
426
+
427
+ /** @type {TopScopedClasses} */
428
+ const topScopedClasses = new Map();
376
429
 
377
430
  context.next({
378
431
  ...context.state,
@@ -388,7 +441,27 @@ const visitors = {
388
441
  analyze_css(css);
389
442
 
390
443
  for (const node of elements) {
391
- prune_css(css, node);
444
+ prune_css(css, node, metadata.styleClasses, topScopedClasses);
445
+ }
446
+
447
+ if (topScopedClasses.size > 0) {
448
+ node.metadata.topScopedClasses = topScopedClasses;
449
+ }
450
+
451
+ if (metadata.styleClasses.size > 0) {
452
+ node.metadata.styleClasses = metadata.styleClasses;
453
+
454
+ if (!context.state.loose) {
455
+ for (const [className, property] of metadata.styleClasses) {
456
+ if (!topScopedClasses?.has(className)) {
457
+ return error(
458
+ `CSS class ".${className}" does not exist in ${node.id?.name ? node.id.name : "this component's"} <style> block`,
459
+ context.state.analysis.module.filename,
460
+ property,
461
+ );
462
+ }
463
+ }
464
+ }
392
465
  }
393
466
  }
394
467
 
@@ -677,13 +750,13 @@ const visitors = {
677
750
 
678
751
  // Store capitalized name for dynamic components/elements
679
752
  if (node.id.tracked) {
680
- const original_name = node.id.name;
681
- const capitalized_name = original_name.charAt(0).toUpperCase() + original_name.slice(1);
753
+ const source_name = node.id.name;
754
+ const capitalized_name = source_name.charAt(0).toUpperCase() + source_name.slice(1);
682
755
  node.metadata.ts_name = capitalized_name;
683
- node.metadata.original_name = original_name;
756
+ node.metadata.source_name = source_name;
684
757
 
685
758
  // Mark the binding as a dynamic component so we can capitalize it everywhere
686
- const binding = context.state.scope.get(original_name);
759
+ const binding = context.state.scope.get(source_name);
687
760
  if (binding) {
688
761
  if (!binding.metadata) {
689
762
  binding.metadata = {};
@@ -889,13 +962,16 @@ export function analyze(ast, filename, options = {}) {
889
962
 
890
963
  const { scope, scopes } = create_scopes(ast, scope_root, null);
891
964
 
892
- const analysis = {
965
+ const analysis = /** @type {AnalysisResult} */ ({
893
966
  module: { ast, scope, scopes, filename },
894
967
  ast,
895
968
  scope,
896
969
  scopes,
897
970
  component_metadata: [],
898
- };
971
+ metadata: {
972
+ serverIdentifierPresent: false,
973
+ },
974
+ });
899
975
 
900
976
  walk(
901
977
  ast,
@@ -1,5 +1,5 @@
1
1
  /** @import * as AST from 'estree' */
2
- /** @import { Visitors } from '#compiler' */
2
+ /** @import { Visitors, TopScopedClasses, StyleClasses } from '#compiler' */
3
3
  /** @typedef {0 | 1} Direction */
4
4
 
5
5
  import { walk } from 'zimmerframe';
@@ -15,6 +15,10 @@ const BACKWARD = 1;
15
15
  // since the code is synchronous, this is safe
16
16
  /** @type {string} */
17
17
  let css_hash;
18
+ /** @type {StyleClasses} */
19
+ let style_identifier_classes;
20
+ /** @type {TopScopedClasses} */
21
+ let top_scoped_classes;
18
22
 
19
23
  // CSS selector constants
20
24
  /**
@@ -27,6 +31,15 @@ function create_descendant_combinator(start, end) {
27
31
  }
28
32
 
29
33
  /**
34
+ * @param {AST.CSS.RelativeSelector} relative_selector
35
+ * @param {AST.CSS.ClassSelector} selector
36
+ * @returns {boolean}
37
+ */
38
+ function is_standalone_class_selector(relative_selector, selector) {
39
+ return relative_selector.selectors.length === 1 && relative_selector.selectors[0] === selector;
40
+ }
41
+
42
+ /**`
30
43
  * @param {number} start
31
44
  * @param {number} end
32
45
  * @returns {AST.CSS.RelativeSelector}
@@ -211,7 +224,6 @@ function apply_selector(relative_selectors, rule, element, direction) {
211
224
  if (!element.metadata.css) {
212
225
  element.metadata.css = {
213
226
  scopedClasses: new Map(),
214
- topScopedClasses: new Map(),
215
227
  hash: css_hash,
216
228
  };
217
229
  }
@@ -225,14 +237,12 @@ function apply_selector(relative_selectors, rule, element, direction) {
225
237
  });
226
238
  }
227
239
 
228
- // Also store in topScopedClasses if standalone
229
- // Standalone = only this ClassSelector in the RelativeSelector, no pseudo-classes/elements
230
- const isStandalone =
231
- relative_selector.selectors.length === 1 &&
232
- relative_selector.selectors[0] === selector;
233
-
234
- if (isStandalone && !element.metadata.css.topScopedClasses.has(name)) {
235
- element.metadata.css.topScopedClasses.set(name, {
240
+ // Also store in top_scoped_classes if standalone selector
241
+ if (
242
+ is_standalone_class_selector(relative_selector, selector) &&
243
+ !top_scoped_classes.has(name)
244
+ ) {
245
+ top_scoped_classes.set(name, {
236
246
  start: selector.start,
237
247
  end: selector.end,
238
248
  selector: selector,
@@ -939,7 +949,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
939
949
  }
940
950
 
941
951
  case 'ClassSelector': {
942
- if (!attribute_matches(element, 'class', name, '~=', false)) {
952
+ if (
953
+ !attribute_matches(element, 'class', name, '~=', false) &&
954
+ (!style_identifier_classes.has(name) ||
955
+ !is_standalone_class_selector(relative_selector, selector))
956
+ ) {
943
957
  return false;
944
958
  }
945
959
 
@@ -1047,10 +1061,14 @@ function rule_has_animation(rule) {
1047
1061
  /**
1048
1062
  * @param {AST.CSS.StyleSheet} css
1049
1063
  * @param {AST.Element} element
1064
+ * @param {StyleClasses} styleClasses
1065
+ * @param {TopScopedClasses} topScopedClasses
1050
1066
  * @return {void}
1051
1067
  */
1052
- export function prune_css(css, element) {
1068
+ export function prune_css(css, element, styleClasses, topScopedClasses) {
1053
1069
  css_hash = css.hash;
1070
+ style_identifier_classes = styleClasses;
1071
+ top_scoped_classes = topScopedClasses;
1054
1072
 
1055
1073
  /** @type {Visitors<AST.CSS.Node, null>} */
1056
1074
  const visitors = {
@@ -151,6 +151,10 @@ export function validate_nesting(element, context) {
151
151
  context.state.analysis.module.filename,
152
152
  element,
153
153
  );
154
+ } else {
155
+ // if my parent has a set of invalid children
156
+ // and i'm not in it, then i'm valid
157
+ return;
154
158
  }
155
159
  }
156
160
  }