ripple 0.2.214 → 0.2.216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/package.json +2 -2
  3. package/src/compiler/errors.js +33 -1
  4. package/src/compiler/index.d.ts +2 -2
  5. package/src/compiler/index.js +8 -1
  6. package/src/compiler/phases/2-analyze/index.js +75 -8
  7. package/src/compiler/phases/2-analyze/prune.js +25 -12
  8. package/src/compiler/phases/2-analyze/validation.js +1 -0
  9. package/src/compiler/phases/3-transform/client/index.js +302 -54
  10. package/src/compiler/phases/3-transform/segments.js +5 -3
  11. package/src/compiler/phases/3-transform/server/index.js +81 -18
  12. package/src/compiler/types/index.d.ts +125 -90
  13. package/src/compiler/utils.js +20 -0
  14. package/src/runtime/internal/client/blocks.js +13 -2
  15. package/src/runtime/internal/client/constants.js +2 -0
  16. package/src/runtime/internal/client/hmr.js +112 -1
  17. package/src/runtime/internal/client/hydration.js +29 -2
  18. package/src/runtime/internal/client/index.js +3 -1
  19. package/src/runtime/internal/client/runtime.js +5 -1
  20. package/src/runtime/internal/client/template.js +30 -68
  21. package/src/runtime/internal/client/try.js +73 -2
  22. package/src/runtime/internal/server/index.js +1 -1
  23. package/tests/client/basic/basic.errors.test.ripple +66 -0
  24. package/tests/client/basic/basic.hmr.test.ripple +19 -0
  25. package/tests/client/compiler/compiler.basic.test.ripple +16 -0
  26. package/tests/client/css/style-identifier.test.ripple +396 -0
  27. package/tests/client/for.test.ripple +41 -0
  28. package/tests/client/switch.test.ripple +50 -0
  29. package/tests/client/try.test.ripple +98 -0
  30. package/tests/hydration/compiled/client/basic.js +10 -10
  31. package/tests/hydration/compiled/client/composite.js +8 -8
  32. package/tests/hydration/compiled/client/for.js +18 -18
  33. package/tests/hydration/compiled/client/head.js +1 -1
  34. package/tests/hydration/compiled/client/hmr.js +86 -0
  35. package/tests/hydration/compiled/client/html-in-template.js +64 -0
  36. package/tests/hydration/compiled/client/html.js +1418 -13
  37. package/tests/hydration/compiled/client/if-children.js +7 -7
  38. package/tests/hydration/compiled/client/if.js +7 -7
  39. package/tests/hydration/compiled/client/mixed-control-flow.js +483 -0
  40. package/tests/hydration/compiled/client/nested-control-flow.js +1457 -0
  41. package/tests/hydration/compiled/client/portal.js +1 -1
  42. package/tests/hydration/compiled/client/reactivity.js +2 -2
  43. package/tests/hydration/compiled/client/return.js +103 -103
  44. package/tests/hydration/compiled/client/switch.js +205 -30
  45. package/tests/hydration/compiled/client/try.js +130 -0
  46. package/tests/hydration/compiled/server/composite.js +2 -0
  47. package/tests/hydration/compiled/server/hmr.js +110 -0
  48. package/tests/hydration/compiled/server/html-in-template.js +96 -0
  49. package/tests/hydration/compiled/server/html.js +1704 -0
  50. package/tests/hydration/compiled/server/if-children.js +4 -0
  51. package/tests/hydration/compiled/server/mixed-control-flow.js +303 -0
  52. package/tests/hydration/compiled/server/nested-control-flow.js +733 -0
  53. package/tests/hydration/compiled/server/portal.js +9 -1
  54. package/tests/hydration/compiled/server/switch.js +153 -0
  55. package/tests/hydration/compiled/server/try.js +140 -0
  56. package/tests/hydration/components/hmr.ripple +35 -0
  57. package/tests/hydration/components/html-in-template.ripple +24 -0
  58. package/tests/hydration/components/html.ripple +390 -1
  59. package/tests/hydration/components/mixed-control-flow.ripple +114 -0
  60. package/tests/hydration/components/nested-control-flow.ripple +269 -0
  61. package/tests/hydration/components/switch.ripple +78 -0
  62. package/tests/hydration/components/try.ripple +31 -0
  63. package/tests/hydration/hmr.test.js +74 -0
  64. package/tests/hydration/html-in-template.test.js +45 -0
  65. package/tests/hydration/html.test.js +116 -20
  66. package/tests/hydration/mixed-control-flow.test.js +70 -0
  67. package/tests/hydration/nested-control-flow.test.js +203 -0
  68. package/tests/hydration/switch.test.js +67 -0
  69. package/tests/hydration/try.test.js +37 -0
  70. package/tests/server/__snapshots__/compiler.test.ripple.snap +3 -1
  71. package/tests/server/await.test.ripple +0 -2
  72. package/tests/server/basic.components.test.ripple +1 -1
  73. package/tests/server/basic.test.ripple +0 -2
  74. package/tests/server/compiler.test.ripple +0 -2
  75. package/tests/server/composite.test.ripple +0 -2
  76. package/tests/server/context.test.ripple +0 -2
  77. package/tests/server/dynamic-elements.test.ripple +2 -2
  78. package/tests/server/for.test.ripple +0 -3
  79. package/tests/server/html-nesting-validation.test.ripple +2 -4
  80. package/tests/server/if.test.ripple +0 -3
  81. package/tests/server/return.test.ripple +0 -3
  82. package/tests/server/streaming-ssr.test.ripple +1 -3
  83. package/tests/server/style-identifier.test.ripple +236 -0
  84. package/tests/server/switch.test.ripple +0 -3
  85. package/tests/server/try.test.ripple +82 -0
  86. package/tests/server/tsconfig.json +1 -0
  87. package/tests/setup-server.js +2 -0
  88. package/tsconfig.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # ripple
2
2
 
3
+ ## 0.2.216
4
+
5
+ ### Patch Changes
6
+
7
+ - [#757](https://github.com/Ripple-TS/ripple/pull/757)
8
+ [`9fb507d`](https://github.com/Ripple-TS/ripple/commit/9fb507d76af6fd6a5c636af1976d1e03d3e869ac)
9
+ Thanks [@leonidaz](https://github.com/leonidaz)! - fixes compiler error that was
10
+ generating async functions for call expressions inside if conditions when inside
11
+ async context
12
+
13
+ - [#751](https://github.com/Ripple-TS/ripple/pull/751)
14
+ [`e1de4bb`](https://github.com/Ripple-TS/ripple/commit/e1de4bb9df75342a693cda24d0999a423db05ec4)
15
+ Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Fix
16
+ HMR "zoom" issue when a Ripple file is changed in the dev server.
17
+
18
+ When a layout component contained children with nested `if`/`for` blocks,
19
+ hydration would leave `hydrate_node` pointing deep inside the layout's root
20
+ element (e.g. a HYDRATION_END comment inside `<main>`). The `append()`
21
+ function's `parentNode === dom` check only handled direct children, so it missed
22
+ grandchild/deeper positions and incorrectly updated the branch block's `s.end`
23
+ to that deep internal node.
24
+
25
+ This caused two problems on HMR re-render:
26
+ 1. `remove_block_dom(s.start, s.end)` removed wrong elements (the deep node was
27
+ treated as a sibling boundary, causing removal of unrelated content including
28
+ the root HYDRATION_END comment).
29
+ 2. `target = hydrate_node` (set after the initial render) became `null` or
30
+ pointed outside the component's region, so new content was inserted at the
31
+ wrong DOM location — producing a layout that appeared "zoomed" because it
32
+ rendered outside its CSS container context.
33
+
34
+ The fix changes the `parentNode === dom` check to `dom.contains(hydrate_node)`,
35
+ consistent with the `anchor === dom` branch that already used `dom.contains()`.
36
+ This correctly resets `hydrate_node` to `dom`'s sibling level regardless of how
37
+ deeply nested it was inside `dom`.
38
+
39
+ - [#764](https://github.com/Ripple-TS/ripple/pull/764)
40
+ [`95ea864`](https://github.com/Ripple-TS/ripple/commit/95ea8645b2cb27e2610a4ace4c8fb238c92d441a)
41
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fixes syntax color
42
+ highlighting for `pending`
43
+
44
+ - Updated dependencies
45
+ [[`9fb507d`](https://github.com/Ripple-TS/ripple/commit/9fb507d76af6fd6a5c636af1976d1e03d3e869ac),
46
+ [`e1de4bb`](https://github.com/Ripple-TS/ripple/commit/e1de4bb9df75342a693cda24d0999a423db05ec4),
47
+ [`95ea864`](https://github.com/Ripple-TS/ripple/commit/95ea8645b2cb27e2610a4ace4c8fb238c92d441a)]:
48
+ - ripple@0.2.216
49
+
50
+ ## 0.2.215
51
+
52
+ ### Patch Changes
53
+
54
+ - [#742](https://github.com/Ripple-TS/ripple/pull/742)
55
+ [`a9ecda4`](https://github.com/Ripple-TS/ripple/commit/a9ecda4e3f29e3b934d9f5ee80d55c059ba36ebe)
56
+ Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Fix
57
+ catch block not executing when used with pending block in try statements.
58
+ Previously, errors thrown inside async components within
59
+ `try { ... } pending { ... } catch { ... }` blocks were lost as unhandled
60
+ promise rejections. Now errors are properly caught and the catch block is
61
+ rendered. Also fixes the server-side rendering to not include pending content in
62
+ the final output when the async operation resolves or errors.
63
+
64
+ - [#744](https://github.com/Ripple-TS/ripple/pull/744)
65
+ [`6653c5c`](https://github.com/Ripple-TS/ripple/commit/6653c5cebfbd4dce129906a25686ef9c63dc592a)
66
+ Thanks [@leonidaz](https://github.com/leonidaz)! - Fix compiler analysis
67
+ incorrectly marking untrackable nodes as tracked. `MemberExpression` now only
68
+ enables tracking when the member or its property is actually marked as
69
+ `tracked`, and unconditional tracking side-effects were removed from
70
+ `CallExpression` and `NewExpression` visitors.
71
+
72
+ Also fixes the client transform for `TrackedExpression` in TypeScript mode to
73
+ emit a `['#v']` member access (marked as `tracked`) instead of the runtime
74
+ `_$_.get(...)` call, aligning TSX output with tracked-access semantics.
75
+
76
+ - [#733](https://github.com/Ripple-TS/ripple/pull/733)
77
+ [`307dcf3`](https://github.com/Ripple-TS/ripple/commit/307dcf30f27dae987a19a59508cc2593c839eda3)
78
+ Thanks [@trueadm](https://github.com/trueadm)! - Fix client HMR updates when a
79
+ wrapped component has not mounted yet. The runtime now avoids calling `set()` on
80
+ an undefined tracked source and keeps wrapper HMR state synchronized across
81
+ update chains.
82
+ - Updated dependencies
83
+ [[`a9ecda4`](https://github.com/Ripple-TS/ripple/commit/a9ecda4e3f29e3b934d9f5ee80d55c059ba36ebe),
84
+ [`6653c5c`](https://github.com/Ripple-TS/ripple/commit/6653c5cebfbd4dce129906a25686ef9c63dc592a),
85
+ [`307dcf3`](https://github.com/Ripple-TS/ripple/commit/307dcf30f27dae987a19a59508cc2593c839eda3)]:
86
+ - ripple@0.2.215
87
+
3
88
  ## 0.2.214
4
89
 
5
90
  ### Patch Changes
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.214",
6
+ "version": "0.2.216",
7
7
  "type": "module",
8
8
  "module": "src/runtime/index-client.js",
9
9
  "main": "src/runtime/index-client.js",
@@ -96,6 +96,6 @@
96
96
  "vscode-languageserver-types": "^3.17.5"
97
97
  },
98
98
  "peerDependencies": {
99
- "ripple": "0.2.214"
99
+ "ripple": "0.2.216"
100
100
  }
101
101
  }
@@ -9,9 +9,14 @@
9
9
  * @param {string | null} filename
10
10
  * @param {AST.Node} node
11
11
  * @param {RippleCompileError[]} [errors]
12
+ * @param {AST.CommentWithLocation[]} [comments]
12
13
  * @returns {void}
13
14
  */
14
- export function error(message, filename, node, errors) {
15
+ export function error(message, filename, node, errors, comments) {
16
+ if (errors && comments && is_ripple_error_suppressed(node, comments)) {
17
+ return;
18
+ }
19
+
15
20
  const error = /** @type {RippleCompileError} */ (new Error(message));
16
21
 
17
22
  // same as the acorn compiler error
@@ -43,3 +48,30 @@ export function error(message, filename, node, errors) {
43
48
  error.type = 'fatal';
44
49
  throw error;
45
50
  }
51
+
52
+ /**
53
+ * @param {AST.CommentWithLocation} comment
54
+ * @return {boolean}
55
+ */
56
+ function is_ripple_error_suppress_comment(comment) {
57
+ const text = comment.value.trim();
58
+ return text.startsWith('@ripple-ignore') || text.startsWith('@ripple-expect-error');
59
+ }
60
+
61
+ /**
62
+ * @param {AST.Node} node
63
+ * @param {AST.CommentWithLocation[]} comments
64
+ */
65
+ function is_ripple_error_suppressed(node, comments) {
66
+ if (node.loc) {
67
+ const node_start_line = node.loc.start.line;
68
+ for (const comment of comments) {
69
+ if (comment.type === 'Line' && comment.loc.start.line === node_start_line - 1) {
70
+ if (is_ripple_error_suppress_comment(comment)) {
71
+ return true;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return false;
77
+ }
@@ -101,6 +101,7 @@ interface SharedCompileOptions {
101
101
  }
102
102
  export interface CompileOptions extends SharedCompileOptions {
103
103
  mode?: 'client' | 'server';
104
+ hmr?: boolean;
104
105
  }
105
106
 
106
107
  export interface ParseOptions {
@@ -115,8 +116,7 @@ export interface AnalyzeOptions extends ParseOptions, Pick<CompileOptions, 'mode
115
116
  }
116
117
 
117
118
  export interface VolarCompileOptions
118
- extends Omit<ParseOptions, 'errors' | 'comments'>,
119
- SharedCompileOptions {}
119
+ extends Omit<ParseOptions, 'errors' | 'comments'>, SharedCompileOptions {}
120
120
 
121
121
  export function parse(source: string, options?: ParseOptions): AST.Program;
122
122
 
@@ -34,7 +34,14 @@ export function compile(source, filename, options = {}) {
34
34
  options?.minify_css ?? false,
35
35
  options?.dev ?? false,
36
36
  )
37
- : transform_client(filename, source, analysis, false, options?.minify_css ?? false);
37
+ : transform_client(
38
+ filename,
39
+ source,
40
+ analysis,
41
+ false,
42
+ options?.minify_css ?? false,
43
+ options?.hmr ?? false,
44
+ );
38
45
 
39
46
  return result;
40
47
  }
@@ -141,6 +141,7 @@ function error_return_keyword(node, context, message) {
141
141
  context.state.analysis.module.filename,
142
142
  return_keyword_node,
143
143
  context.state.loose ? context.state.analysis.errors : undefined,
144
+ context.state.analysis.comments,
144
145
  );
145
146
  }
146
147
 
@@ -281,6 +282,7 @@ const visitors = {
281
282
  context.state.analysis.module.filename,
282
283
  node,
283
284
  context.state.loose ? context.state.analysis.errors : undefined,
285
+ context.state.analysis.comments,
284
286
  );
285
287
  }
286
288
  }
@@ -307,7 +309,13 @@ const visitors = {
307
309
  MemberExpression(node, context) {
308
310
  const parent = context.path.at(-1);
309
311
 
310
- if (context.state.metadata?.tracking === false && parent?.type !== 'AssignmentExpression') {
312
+ if (
313
+ context.state.metadata?.tracking === false &&
314
+ parent?.type !== 'AssignmentExpression' &&
315
+ (node.tracked ||
316
+ ((node.property.type === 'Identifier' || node.property.type === 'Literal') &&
317
+ /** @type {AST.TrackedNode} */ (node.property).tracked))
318
+ ) {
311
319
  context.state.metadata.tracking = true;
312
320
  }
313
321
 
@@ -321,6 +329,7 @@ const visitors = {
321
329
  context.state.analysis.module.filename,
322
330
  node,
323
331
  context.state.loose ? context.state.analysis.errors : undefined,
332
+ context.state.analysis.comments,
324
333
  );
325
334
  } else {
326
335
  component.metadata.styleIdentifierPresent = true;
@@ -346,6 +355,7 @@ const visitors = {
346
355
  context.state.analysis.module.filename,
347
356
  node.property,
348
357
  context.state.loose ? context.state.analysis.errors : undefined,
358
+ context.state.analysis.comments,
349
359
  );
350
360
  }
351
361
 
@@ -377,6 +387,7 @@ const visitors = {
377
387
  context.state.analysis.module.filename,
378
388
  node.property,
379
389
  context.state.loose ? context.state.analysis.errors : undefined,
390
+ context.state.analysis.comments,
380
391
  );
381
392
  }
382
393
  }
@@ -391,6 +402,7 @@ const visitors = {
391
402
  context.state.analysis.module.filename,
392
403
  node.object,
393
404
  context.state.loose ? context.state.analysis.errors : undefined,
405
+ context.state.analysis.comments,
394
406
  );
395
407
  }
396
408
  }
@@ -416,13 +428,10 @@ const visitors = {
416
428
  context.state.analysis.module.filename,
417
429
  node.callee,
418
430
  context.state.loose ? context.state.analysis.errors : undefined,
431
+ context.state.analysis.comments,
419
432
  );
420
433
  }
421
434
 
422
- if (context.state.metadata?.tracking === false) {
423
- context.state.metadata.tracking = true;
424
- }
425
-
426
435
  if (!is_inside_component(context, true)) {
427
436
  mark_as_tracked(context.path);
428
437
  }
@@ -431,9 +440,6 @@ const visitors = {
431
440
  },
432
441
 
433
442
  NewExpression(node, context) {
434
- if (context.state.metadata?.tracking === false) {
435
- context.state.metadata.tracking = true;
436
- }
437
443
  context.next();
438
444
  },
439
445
 
@@ -447,6 +453,7 @@ const visitors = {
447
453
  state.analysis.module.filename,
448
454
  declarator.id,
449
455
  context.state.loose ? context.state.analysis.errors : undefined,
456
+ context.state.analysis.comments,
450
457
  );
451
458
  }
452
459
  const metadata = { tracking: false, await: false };
@@ -477,6 +484,7 @@ const visitors = {
477
484
  state.analysis.module.filename,
478
485
  path.node,
479
486
  context.state.loose ? context.state.analysis.errors : undefined,
487
+ context.state.analysis.comments,
480
488
  );
481
489
  }
482
490
  }
@@ -536,6 +544,7 @@ const visitors = {
536
544
  context.state.analysis.module.filename,
537
545
  props,
538
546
  context.state.loose ? context.state.analysis.errors : undefined,
547
+ context.state.analysis.comments,
539
548
  );
540
549
  }
541
550
  }
@@ -582,6 +591,7 @@ const visitors = {
582
591
  context.state.analysis.module.filename,
583
592
  property,
584
593
  context.state.loose ? context.state.analysis.errors : undefined,
594
+ context.state.analysis.comments,
585
595
  );
586
596
  }
587
597
  }
@@ -639,6 +649,7 @@ const visitors = {
639
649
  context.state.analysis.module.filename,
640
650
  switch_case,
641
651
  context.state.loose ? context.state.analysis.errors : undefined,
652
+ context.state.analysis.comments,
642
653
  );
643
654
  }
644
655
  }
@@ -712,6 +723,7 @@ const visitors = {
712
723
  context.state.analysis.module.filename,
713
724
  node.body,
714
725
  context.state.loose ? context.state.analysis.errors : undefined,
726
+ context.state.analysis.comments,
715
727
  );
716
728
  }
717
729
  },
@@ -734,6 +746,7 @@ const visitors = {
734
746
  context.state.analysis.module.filename,
735
747
  /** @type {AST.Identifier} */ (declaration.id),
736
748
  context.state.loose ? context.state.analysis.errors : undefined,
749
+ context.state.analysis.comments,
737
750
  );
738
751
  // TODO: the client and server rendering doesn't currently support components
739
752
  // If we're going to support this, we need to account also for anonymous object declaration
@@ -762,6 +775,7 @@ const visitors = {
762
775
  context.state.analysis.module.filename,
763
776
  decl.init,
764
777
  context.state.loose ? context.state.analysis.errors : undefined,
778
+ context.state.analysis.comments,
765
779
  );
766
780
  continue;
767
781
  }
@@ -773,6 +787,7 @@ const visitors = {
773
787
  context.state.analysis.module.filename,
774
788
  path.node,
775
789
  context.state.loose ? context.state.analysis.errors : undefined,
790
+ context.state.analysis.comments,
776
791
  );
777
792
  }
778
793
  }
@@ -783,6 +798,7 @@ const visitors = {
783
798
  context.state.analysis.module.filename,
784
799
  decl,
785
800
  context.state.loose ? context.state.analysis.errors : undefined,
801
+ context.state.analysis.comments,
786
802
  );
787
803
  }
788
804
  } else if (node.specifiers) {
@@ -801,6 +817,7 @@ const visitors = {
801
817
  context.state.analysis.module.filename,
802
818
  specifier,
803
819
  context.state.loose ? context.state.analysis.errors : undefined,
820
+ context.state.analysis.comments,
804
821
  );
805
822
  }
806
823
  } else {
@@ -809,6 +826,7 @@ const visitors = {
809
826
  context.state.analysis.module.filename,
810
827
  node,
811
828
  context.state.loose ? context.state.analysis.errors : undefined,
829
+ context.state.analysis.comments,
812
830
  );
813
831
  }
814
832
 
@@ -863,6 +881,7 @@ const visitors = {
863
881
  context.state.analysis.module.filename,
864
882
  node.consequent,
865
883
  context.state.loose ? context.state.analysis.errors : undefined,
884
+ context.state.analysis.comments,
866
885
  );
867
886
  }
868
887
 
@@ -879,6 +898,7 @@ const visitors = {
879
898
  context.state.analysis.module.filename,
880
899
  node.alternate,
881
900
  context.state.loose ? context.state.analysis.errors : undefined,
901
+ context.state.analysis.comments,
882
902
  );
883
903
  }
884
904
 
@@ -966,6 +986,7 @@ const visitors = {
966
986
  state.analysis.module.filename,
967
987
  node.block,
968
988
  context.state.loose ? context.state.analysis.errors : undefined,
989
+ context.state.analysis.comments,
969
990
  );
970
991
  }
971
992
 
@@ -982,6 +1003,7 @@ const visitors = {
982
1003
  state.analysis.module.filename,
983
1004
  node.pending,
984
1005
  context.state.loose ? context.state.analysis.errors : undefined,
1006
+ context.state.analysis.comments,
985
1007
  );
986
1008
  }
987
1009
  }
@@ -1009,6 +1031,30 @@ const visitors = {
1009
1031
  context.next();
1010
1032
  },
1011
1033
 
1034
+ WhileStatement(node, context) {
1035
+ if (is_inside_component(context)) {
1036
+ error(
1037
+ 'While loops are not supported in components. Move the while loop into a function.',
1038
+ context.state.analysis.module.filename,
1039
+ node,
1040
+ );
1041
+ }
1042
+
1043
+ context.next();
1044
+ },
1045
+
1046
+ DoWhileStatement(node, context) {
1047
+ if (is_inside_component(context)) {
1048
+ error(
1049
+ 'Do...while loops are not supported in components. Move the do...while loop into a function.',
1050
+ context.state.analysis.module.filename,
1051
+ node,
1052
+ );
1053
+ }
1054
+
1055
+ context.next();
1056
+ },
1057
+
1012
1058
  JSXElement(node, context) {
1013
1059
  const inside_tsx_compat = context.path.some((n) => n.type === 'TsxCompat');
1014
1060
 
@@ -1167,6 +1213,7 @@ const visitors = {
1167
1213
  },
1168
1214
  },
1169
1215
  context.state.loose ? context.state.analysis.errors : undefined,
1216
+ context.state.analysis.comments,
1170
1217
  );
1171
1218
  }
1172
1219
  if (attr.name.type === 'Identifier') {
@@ -1178,6 +1225,21 @@ const visitors = {
1178
1225
  state.analysis.module.filename,
1179
1226
  attr,
1180
1227
  context.state.loose ? context.state.analysis.errors : undefined,
1228
+ context.state.analysis.comments,
1229
+ );
1230
+ }
1231
+
1232
+ if (
1233
+ attr.value &&
1234
+ attr.value.type === 'MemberExpression' &&
1235
+ attr.value.object.type === 'StyleIdentifier'
1236
+ ) {
1237
+ error(
1238
+ '`#style` cannot be used directly on DOM elements. Pass the class to a child component instead.',
1239
+ state.analysis.module.filename,
1240
+ attr.value.object,
1241
+ context.state.loose ? context.state.analysis.errors : undefined,
1242
+ context.state.analysis.comments,
1181
1243
  );
1182
1244
  }
1183
1245
 
@@ -1205,6 +1267,7 @@ const visitors = {
1205
1267
  state.analysis.module.filename,
1206
1268
  node,
1207
1269
  context.state.loose ? context.state.analysis.errors : undefined,
1270
+ context.state.analysis.comments,
1208
1271
  );
1209
1272
  }
1210
1273
  } else {
@@ -1246,6 +1309,7 @@ const visitors = {
1246
1309
  state.analysis.module.filename,
1247
1310
  item,
1248
1311
  context.state.loose ? context.state.analysis.errors : undefined,
1312
+ context.state.analysis.comments,
1249
1313
  );
1250
1314
  }
1251
1315
  }
@@ -1261,6 +1325,7 @@ const visitors = {
1261
1325
  state.analysis.module.filename,
1262
1326
  attribute,
1263
1327
  context.state.loose ? context.state.analysis.errors : undefined,
1328
+ context.state.analysis.comments,
1264
1329
  );
1265
1330
  }
1266
1331
  }
@@ -1286,6 +1351,7 @@ const visitors = {
1286
1351
  context.state.analysis.module.filename,
1287
1352
  node.expression,
1288
1353
  context.state.loose ? context.state.analysis.errors : undefined,
1354
+ context.state.analysis.comments,
1289
1355
  );
1290
1356
  }
1291
1357
 
@@ -1320,6 +1386,7 @@ const visitors = {
1320
1386
  context.state.analysis.module.filename,
1321
1387
  adjusted_node,
1322
1388
  context.state.loose ? context.state.analysis.errors : undefined,
1389
+ context.state.analysis.comments,
1323
1390
  );
1324
1391
  }
1325
1392
  }
@@ -236,18 +236,6 @@ function apply_selector(relative_selectors, rule, element, direction) {
236
236
  selector: selector,
237
237
  });
238
238
  }
239
-
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, {
246
- start: selector.start,
247
- end: selector.end,
248
- selector: selector,
249
- });
250
- }
251
239
  }
252
240
  }
253
241
  }
@@ -1090,6 +1078,31 @@ export function prune_css(css, element, styleClasses, topScopedClasses) {
1090
1078
  node.metadata.used = true;
1091
1079
  }
1092
1080
 
1081
+ // Populate top_scoped_classes for truly standalone class selectors (for #style support).
1082
+ // A class is standalone only when the entire effective selector chain (after resolving
1083
+ // nesting and stripping :global) is a single RelativeSelector with a single ClassSelector.
1084
+ // This prevents classes from compound selectors like `.wrapper .nested` or selectors
1085
+ // inside :global() from being treated as valid #style targets.
1086
+ if (selectors.length === 1) {
1087
+ const sole_selector = selectors[0];
1088
+ if (
1089
+ !sole_selector.metadata.is_global &&
1090
+ !sole_selector.metadata.is_global_like &&
1091
+ sole_selector.selectors.length === 1 &&
1092
+ sole_selector.selectors[0].type === 'ClassSelector'
1093
+ ) {
1094
+ const class_selector = sole_selector.selectors[0];
1095
+ const name = class_selector.name.replace(regex_backslash_and_following_character, '$1');
1096
+ if (!top_scoped_classes.has(name)) {
1097
+ top_scoped_classes.set(name, {
1098
+ start: class_selector.start,
1099
+ end: class_selector.end,
1100
+ selector: class_selector,
1101
+ });
1102
+ }
1103
+ }
1104
+ }
1105
+
1093
1106
  context.next();
1094
1107
  },
1095
1108
  PseudoClassSelector(node, context) {
@@ -155,6 +155,7 @@ export function validate_nesting(element, context, errors) {
155
155
  context.state.analysis.module.filename,
156
156
  element,
157
157
  errors,
158
+ context.state.analysis.comments,
158
159
  );
159
160
  } else {
160
161
  // if my parent has a set of invalid children