@tsrx/core 0.1.65 → 0.1.67

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 (55) hide show
  1. package/README.md +26 -1
  2. package/package.json +9 -5
  3. package/src/analyze/css-analyze.js +44 -6
  4. package/src/analyze/index.js +14 -1
  5. package/src/analyze/prune.js +36 -9
  6. package/src/analyze/style-analyze.js +467 -0
  7. package/src/analyze/validation.js +57 -0
  8. package/src/diagnostics.js +22 -0
  9. package/src/index.js +18 -0
  10. package/src/parse/style.js +97 -7
  11. package/src/plugin.js +145 -47
  12. package/src/scope.js +1 -1
  13. package/src/transform/jsx/index.js +94 -417
  14. package/src/transform/jsx/style-scopes.js +866 -0
  15. package/src/transform/scoping.js +129 -79
  16. package/src/transform/segments.js +16 -4
  17. package/src/transform/style-ref.js +74 -13
  18. package/src/transform/stylesheet.js +2 -1
  19. package/src/utils/is-reference.js +59 -0
  20. package/tests/fixtures/scoped-styles/README.md +71 -0
  21. package/tests/fixtures/scoped-styles/apply-forms.expected.json +18 -0
  22. package/tests/fixtures/scoped-styles/apply-forms.tsrx +69 -0
  23. package/tests/fixtures/scoped-styles/assigned-positions.expected.json +29 -0
  24. package/tests/fixtures/scoped-styles/assigned-positions.tsrx +90 -0
  25. package/tests/fixtures/scoped-styles/class-opt-in.expected.json +13 -0
  26. package/tests/fixtures/scoped-styles/class-opt-in.tsrx +39 -0
  27. package/tests/fixtures/scoped-styles/control-flow-else-if.expected.json +11 -0
  28. package/tests/fixtures/scoped-styles/control-flow-else-if.tsrx +26 -0
  29. package/tests/fixtures/scoped-styles/control-flow.expected.json +17 -0
  30. package/tests/fixtures/scoped-styles/control-flow.tsrx +102 -0
  31. package/tests/fixtures/scoped-styles/cross-module-apply.expected.json +14 -0
  32. package/tests/fixtures/scoped-styles/cross-module-apply.tsrx +48 -0
  33. package/tests/fixtures/scoped-styles/element-rooted-templates.expected.json +12 -0
  34. package/tests/fixtures/scoped-styles/element-rooted-templates.tsrx +33 -0
  35. package/tests/fixtures/scoped-styles/precedence.expected.json +11 -0
  36. package/tests/fixtures/scoped-styles/precedence.tsrx +45 -0
  37. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.expected.json +12 -0
  38. package/tests/fixtures/scoped-styles/rfc-opening-example/panel.tsrx +46 -0
  39. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.expected.json +9 -0
  40. package/tests/fixtures/scoped-styles/rfc-opening-example/theme.tsrx +24 -0
  41. package/tests/fixtures/scoped-styles/search-panel.expected.json +11 -0
  42. package/tests/fixtures/scoped-styles/search-panel.tsrx +48 -0
  43. package/tests/fixtures/scoped-styles/sibling-combinators.expected.json +16 -0
  44. package/tests/fixtures/scoped-styles/sibling-combinators.tsrx +42 -0
  45. package/tests/fixtures/scoped-styles/sibling-scope.expected.json +11 -0
  46. package/tests/fixtures/scoped-styles/sibling-scope.tsrx +44 -0
  47. package/tests/fixtures/scoped-styles/sibling-scopes.expected.json +12 -0
  48. package/tests/fixtures/scoped-styles/sibling-scopes.tsrx +51 -0
  49. package/tests/fixtures/scoped-styles/theme-composition.expected.json +14 -0
  50. package/tests/fixtures/scoped-styles/theme-composition.tsrx +45 -0
  51. package/tests/fixtures/scoped-styles/theme-diamond.expected.json +10 -0
  52. package/tests/fixtures/scoped-styles/theme-diamond.tsrx +18 -0
  53. package/tests/shared/scoped-styles-fixtures.js +67 -0
  54. package/tests/utils/fixtures/style-syntax.js +519 -0
  55. package/types/index.d.ts +85 -0
@@ -1,13 +1,12 @@
1
1
  /** @import * as AST from 'estree' */
2
2
  /** @import * as ESTreeJSX from 'estree-jsx' */
3
- /** @import { BaseNodeMetaData, FunctionMetaData, JsxHelperComponent, JsxHelperState, JsxPlatform, JsxStyleContext, JsxTransformContext as TransformContext, JsxTransformOptions, JsxTransformResult, JsxVisitorContext } from '@tsrx/core/types' */
3
+ /** @import { BaseNodeMetaData, FunctionMetaData, JsxHelperComponent, JsxHelperState, JsxPlatform, JsxTransformContext as TransformContext, JsxTransformOptions, JsxTransformResult, JsxVisitorContext } from '@tsrx/core/types' */
4
4
 
5
5
  import { walk } from 'zimmerframe';
6
6
  import { print } from 'esrap';
7
7
  import { error } from '../../errors.js';
8
8
  import { is_template_value_position } from '../../analyze/validation.js';
9
9
  import { analyze_css } from '../../analyze/css-analyze.js';
10
- import { prune_css } from '../../analyze/prune.js';
11
10
  import {
12
11
  in_jsx_child_context,
13
12
  is_empty_jsx_fragment,
@@ -44,12 +43,11 @@ import {
44
43
  find_first_top_level_await,
45
44
  find_first_top_level_await_in_tsrx_function_body,
46
45
  } from '../await.js';
47
- import { prepare_stylesheet_for_render, annotate_with_hash, is_style_element } from '../scoping.js';
46
+ import { prepare_stylesheet_for_render, is_style_element } from '../scoping.js';
47
+ import { prepare_style_scopes, type_only_style } from './style-scopes.js';
48
48
  import {
49
- collect_style_ref_attributes,
50
- create_style_class_map,
49
+ build_style_class_map,
51
50
  create_style_class_map_from_stylesheet,
52
- create_style_ref_setup_statements,
53
51
  get_style_element_stylesheet,
54
52
  } from '../style-ref.js';
55
53
  import {
@@ -635,6 +633,12 @@ export function createJsxTransform(platform) {
635
633
  ast = lower_server_module_for_types(ast, platform.serverModule);
636
634
  }
637
635
 
636
+ // Style scopes: which `<style>` blocks scope which template, CSS emission
637
+ // order, and the classes every element carries. Runs once, before any
638
+ // lowering, so order is an invariant of source structure (see
639
+ // style-scopes.js). Copy-on-write like the passes below.
640
+ ast = prepare_style_scopes(ast, transform_context);
641
+
638
642
  // Both passes are copy-on-write over arbitrary property values, so they
639
643
  // hand back the same shape they were given.
640
644
  ast = /** @type {AST.Program} */ (expand_child_code_blocks(ast));
@@ -668,10 +672,7 @@ export function createJsxTransform(platform) {
668
672
  return visit(create_native_tsrx_render_block(node, state), state);
669
673
  }
670
674
 
671
- const style_context = prepare_tsrx_fragment_styles(node, state);
672
- const target = /** @type {AST.TSRXJSXElement | AST.TSRXJSXFragment} */ (
673
- style_context?.fragment ?? next() ?? node
674
- );
675
+ const target = /** @type {AST.TSRXJSXElement | AST.TSRXJSXFragment} */ (next() ?? node);
675
676
  // An EMPTY fragment that is the sole expression of a `{ … }` container in a
676
677
  // JSX child slot (`<b>{<></>}</b>`) must stay `<></>`: the container already
677
678
  // supplies the `{}` wrapper, so lowering it to a bare `null` (the default
@@ -701,11 +702,7 @@ export function createJsxTransform(platform) {
701
702
  ) {
702
703
  expression = wrap_lowered_value_in_fragment(expression, node);
703
704
  }
704
- for (const statement of create_tsrx_style_ref_setup_statements(
705
- target,
706
- style_context,
707
- state,
708
- )) {
705
+ for (const statement of take_style_ref_statements(target)) {
709
706
  add_jsx_setup_declaration(expression, statement);
710
707
  }
711
708
  return wrap_jsx_setup_declarations(expression, in_jsx_child);
@@ -776,17 +773,41 @@ export function createJsxTransform(platform) {
776
773
  JSXStyleElement(node, { path, state }) {
777
774
  if (is_style_expression_position(path)) {
778
775
  const stylesheet = get_style_element_stylesheet(node);
779
- if (stylesheet) {
776
+ // The scope pre-pass renders assigned blocks at their declaration
777
+ // position; a block it did not reach (generated after it ran) is
778
+ // rendered here, at the end of the emission order.
779
+ if (stylesheet && !node.metadata.tsrx_style_prepared) {
780
+ node.metadata.tsrx_style_prepared = true;
780
781
  analyze_css(stylesheet);
781
- state.stylesheets.push(prepare_stylesheet_for_render(stylesheet, true));
782
- return create_style_expression_value(node, stylesheet, state);
782
+ state.stylesheets.push(
783
+ prepare_stylesheet_for_render(
784
+ stylesheet,
785
+ node.metadata.styleKind === 'theme' ? 'theme' : 'class-map',
786
+ ),
787
+ );
783
788
  }
789
+ return create_style_expression_value(node, stylesheet, state);
784
790
  }
785
- return b.jsx_element(
786
- /** @type {ESTreeJSX.JSXElement} */ ({ ...node, type: 'JSXElement', children: [] }),
787
- node.openingElement?.attributes ?? [],
791
+ // A scoped block never reaches the output: the pre-pass strips it.
792
+ // What is left is the type-only stand-in (kept for editor mappings)
793
+ // or a block outside any scope, which is already reported.
794
+ const stand_in = state.typeOnly ? type_only_style(node) : node;
795
+ const element = b.jsx_element(
796
+ /** @type {ESTreeJSX.JSXElement} */ ({ ...stand_in, type: 'JSXElement', children: [] }),
797
+ stand_in.openingElement?.attributes ?? [],
788
798
  [],
789
799
  );
800
+ // A stand-in in a statement slot (a `<style>` sibling of the output
801
+ // node) prints as an expression statement; two in a row would parse as
802
+ // adjacent JSX (TS2657), so each is its own `void` expression.
803
+ const parent = path.at(-1);
804
+ const in_statement_slot =
805
+ parent?.type === 'JSXCodeBlock' ||
806
+ parent?.type === 'BlockStatement' ||
807
+ parent?.type === 'SwitchCase' ||
808
+ parent?.type === 'Program' ||
809
+ parent?.type === 'ExpressionStatement';
810
+ return state.typeOnly && in_statement_slot ? b.unary('void', element) : element;
790
811
  },
791
812
 
792
813
  JSXCodeBlock: transform_jsx_code_block,
@@ -1083,99 +1104,6 @@ function inject_dynamic_import(program, transform_context) {
1083
1104
  );
1084
1105
  }
1085
1106
 
1086
- /**
1087
- * Attach selector-location metadata used by editor definitions/hover before
1088
- * the shared scoping pass mutates class attributes with the component hash.
1089
- *
1090
- * @param {AST.NativeTSRXNode} component
1091
- * @param {AST.CSS.StyleSheet} css
1092
- * @param {TransformContext} transform_context
1093
- * @param {boolean} [export_top_scoped_classes]
1094
- * @param {string} [region_hash]
1095
- * @returns {void}
1096
- */
1097
- function apply_css_definition_metadata(
1098
- component,
1099
- css,
1100
- transform_context,
1101
- export_top_scoped_classes = false,
1102
- region_hash = css.hash,
1103
- ) {
1104
- analyze_css(css);
1105
-
1106
- const metadata = component.metadata || (component.metadata = { path: [] });
1107
- const style_classes = metadata.styleClasses || (metadata.styleClasses = new Map());
1108
- const top_scoped_classes = metadata.topScopedClasses || new Map();
1109
- const elements = collect_css_prunable_elements(node_children(component), [], transform_context);
1110
-
1111
- const prune = () => {
1112
- for (const element of elements) {
1113
- prune_css(css, element, style_classes, top_scoped_classes, region_hash);
1114
- }
1115
- };
1116
-
1117
- prune();
1118
-
1119
- if (export_top_scoped_classes) {
1120
- for (const [class_name, class_info] of top_scoped_classes) {
1121
- style_classes.set(class_name, class_info.selector ?? class_info);
1122
- }
1123
- prune();
1124
- }
1125
-
1126
- if (top_scoped_classes.size > 0) {
1127
- metadata.topScopedClasses = top_scoped_classes;
1128
- }
1129
- }
1130
-
1131
- /**
1132
- * Pruning runs from the fragment visitor, before the walker has descended into
1133
- * the subtree and stamped walker paths, so each collected element gets its
1134
- * ancestor chain (`metadata.path`) here — descendant/sibling selector matching
1135
- * in `prune_css` reads it.
1136
- *
1137
- * @param {AST.Node | AST.Node[]} value
1138
- * @param {AST.TSRXJSXElement[]} [elements]
1139
- * @param {TransformContext | null} [transform_context]
1140
- * @param {AST.Node[]} [path]
1141
- * @returns {AST.TSRXJSXElement[]}
1142
- */
1143
- function collect_css_prunable_elements(value, elements = [], transform_context = null, path = []) {
1144
- if (Array.isArray(value)) {
1145
- for (const child of value) {
1146
- collect_css_prunable_elements(child, elements, transform_context, path);
1147
- }
1148
- return elements;
1149
- }
1150
-
1151
- if (
1152
- (value.type === 'FunctionDeclaration' ||
1153
- value.type === 'FunctionExpression' ||
1154
- value.type === 'ArrowFunctionExpression') &&
1155
- // Generated dynamic-tag wrappers are render-block closures, not user
1156
- // component boundaries — the element inside still belongs to this
1157
- // component's scoped CSS.
1158
- value.metadata?.tsrx_dynamic_wrapper !== true
1159
- ) {
1160
- return elements;
1161
- }
1162
-
1163
- if (value.type === 'JSXElement' && value.metadata?.native_tsrx) {
1164
- if (!is_style_element(value)) {
1165
- set_node_path_metadata(value, path);
1166
- elements.push(value);
1167
- }
1168
- }
1169
-
1170
- const child_path = [...path, value];
1171
-
1172
- for (const child of child_nodes(value, 'css')) {
1173
- collect_css_prunable_elements(child, elements, transform_context, child_path);
1174
- }
1175
-
1176
- return elements;
1177
- }
1178
-
1179
1107
  /**
1180
1108
  * @param {AST.Node[]} body_nodes
1181
1109
  * @param {TransformContext} transform_context
@@ -2318,112 +2246,20 @@ function node_contains_native_tsrx_template(node) {
2318
2246
  }
2319
2247
 
2320
2248
  /**
2321
- * @param {AST.NativeTSRXNode} node
2322
- * @param {boolean} allow_multiple
2323
- * @returns {AST.CSS.StyleSheet[] | null}
2324
- */
2325
- function collect_tsrx_stylesheets(node, allow_multiple) {
2326
- /** @type {AST.CSS.StyleSheet[]} */
2327
- const styles = [];
2328
- collect_style_elements(node_children(node), styles);
2329
-
2330
- if (styles.length === 0) return null;
2331
- if (styles.length > 1 && !allow_multiple) {
2332
- throw new Error('TSRX fragments can only have one style tag');
2333
- }
2334
-
2335
- return styles;
2336
- }
2337
-
2338
- /**
2339
- * @param {AST.NativeTSRXNode} node
2340
- * @param {TransformContext} transform_context
2341
- * @returns {JsxStyleContext | null}
2342
- */
2343
- function prepare_tsrx_fragment_styles(node, transform_context) {
2344
- // Type-only output must stay analyzable: a throw here makes language hosts
2345
- // (tsrx-tsc, editors) fall back to presenting the RAW source as the virtual
2346
- // TSX, so every CSS brace in the file becomes a TSX parse error. Platforms
2347
- // whose runtime dialect allows one scope to split its CSS across several
2348
- // `<style>` tags are valid input here, and platforms that forbid it still
2349
- // report the rule through their own runtime compiler.
2350
- const sheets = collect_tsrx_stylesheets(node, transform_context.typeOnly);
2351
- if (!sheets) return null;
2352
-
2353
- const css = sheets[0];
2354
- const style_refs = collect_style_ref_attributes(node);
2355
- // A component scope keeps ONE hash even when its CSS is split across
2356
- // several `<style>` tags: rebase every sheet onto the first sheet's hash
2357
- // before scoping, so selector rewriting and the DOM hash annotation below
2358
- // agree. `apply_css_definition_metadata` accumulates into the component's
2359
- // `styleClasses`/`topScopedClasses` metadata, so per-sheet calls compose
2360
- // into one class map for style refs.
2361
- for (const sheet of sheets) {
2362
- const region_hash = sheet.hash;
2363
- sheet.hash = css.hash;
2364
- // `prune_css` inside marks the matching selectors as used/scoped; selectors
2365
- // that match no element render commented out, matching target behavior.
2366
- apply_css_definition_metadata(
2367
- node,
2368
- sheet,
2369
- transform_context,
2370
- style_refs.length > 0,
2371
- region_hash,
2372
- );
2373
- transform_context.stylesheets.push(sheet);
2374
- }
2375
- const fragment = annotate_tsrx_with_hash(
2376
- node,
2377
- css.hash,
2378
- transform_context.platform.jsx.classAttrName ??
2379
- (transform_context.platform.jsx.rewriteClassAttr ? 'className' : 'class'),
2380
- transform_context.typeOnly,
2381
- );
2382
- return { css, style_refs, fragment };
2383
- }
2384
-
2385
- /**
2386
- * @template T
2387
- * @param {AST.NativeTSRXNode} node
2388
- * @param {TransformContext} transform_context
2389
- * @param {(style_context: JsxStyleContext | null) => T} callback
2390
- * @returns {T}
2391
- */
2392
- function with_tsrx_fragment_styles(node, transform_context, callback) {
2393
- const style_context = prepare_tsrx_fragment_styles(node, transform_context);
2394
- return callback(style_context);
2395
- }
2396
-
2397
- /**
2398
- * @param {AST.Node} fragment
2399
- * @param {JsxStyleContext | null} style_context
2400
- * @param {TransformContext} transform_context
2401
- * @returns {AST.Statement[]}
2402
- */
2403
- function create_tsrx_style_ref_setup_statements(fragment, style_context, transform_context) {
2404
- if (!style_context || style_context.style_refs.length === 0) {
2405
- return [];
2406
- }
2407
-
2408
- return create_style_ref_setup_statements(
2409
- style_context.style_refs,
2410
- create_style_class_map(fragment, style_context.css),
2411
- {
2412
- allowMutableRefTarget: transform_context.platform.jsx.multiRefStrategy === 'array',
2413
- createTempIdentifier: () =>
2414
- create_generated_identifier(create_style_ref_temp_name(transform_context)),
2415
- },
2416
- );
2417
- }
2418
-
2419
- /**
2249
+ * The object an assigned block evaluates to: `$class` (applied themes'
2250
+ * classes, then the own hash) followed by one entry per exposed class. A
2251
+ * body-less `<style apply={…} />` exposes `$class` only.
2252
+ *
2420
2253
  * @param {AST.JSXStyleElement} node
2421
- * @param {AST.CSS.StyleSheet} stylesheet
2254
+ * @param {AST.CSS.StyleSheet | null} stylesheet
2422
2255
  * @param {TransformContext} transform_context
2423
2256
  * @returns {AST.Expression}
2424
2257
  */
2425
2258
  function create_style_expression_value(node, stylesheet, transform_context) {
2426
- const class_map = create_style_class_map_from_stylesheet(stylesheet);
2259
+ const options = { applied: node.metadata.tsrx_style_class_parts ?? [] };
2260
+ const class_map = stylesheet
2261
+ ? create_style_class_map_from_stylesheet(stylesheet, options)
2262
+ : build_style_class_map(new Map(), null, options);
2427
2263
  if (!transform_context.typeOnly) {
2428
2264
  return class_map;
2429
2265
  }
@@ -2437,7 +2273,7 @@ function create_style_expression_value(node, stylesheet, transform_context) {
2437
2273
  * @param {TransformContext} transform_context
2438
2274
  */
2439
2275
  function add_type_only_style_anchor(node, transform_context) {
2440
- const style_anchor = b.jsx_element(clone_ast_node(node, true), [], []);
2276
+ const style_anchor = b.jsx_element(clone_ast_node(type_only_style(node), true), [], []);
2441
2277
  disable_style_anchor_verification(style_anchor);
2442
2278
 
2443
2279
  const anchor_id = create_generated_identifier(create_style_anchor_name(transform_context));
@@ -2474,177 +2310,6 @@ function disable_style_anchor_verification(element) {
2474
2310
  }
2475
2311
  }
2476
2312
 
2477
- /**
2478
- * @param {TransformContext} transform_context
2479
- * @returns {string}
2480
- */
2481
- function create_style_ref_temp_name(transform_context) {
2482
- if (transform_context.helper_state) {
2483
- return create_helper_name(transform_context.helper_state, 'style_ref');
2484
- }
2485
-
2486
- transform_context.local_statement_component_index += 1;
2487
- return `_tsrx_style_ref_${transform_context.local_statement_component_index}`;
2488
- }
2489
-
2490
- /**
2491
- * @param {AST.Node | AST.Node[] | null | undefined} node
2492
- * @param {AST.CSS.StyleSheet[]} styles
2493
- * @returns {void}
2494
- */
2495
- function collect_style_elements(node, styles) {
2496
- if (!node) return;
2497
-
2498
- if (Array.isArray(node)) {
2499
- for (const child of node) {
2500
- collect_style_elements(child, styles);
2501
- }
2502
- return;
2503
- }
2504
-
2505
- if (is_style_element(node)) {
2506
- const stylesheet = node.children?.find((child) => child.type === 'StyleSheet');
2507
- if (stylesheet) {
2508
- styles.push(stylesheet);
2509
- }
2510
- return;
2511
- }
2512
-
2513
- if (is_function_or_class_boundary(node)) {
2514
- return;
2515
- }
2516
-
2517
- if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
2518
- collect_style_elements(node.children || [], styles);
2519
- return;
2520
- }
2521
-
2522
- if (node.type === 'BlockStatement') {
2523
- collect_style_elements(node.body || [], styles);
2524
- return;
2525
- }
2526
-
2527
- if (is_if_control_node(node)) {
2528
- collect_style_elements(node.consequent, styles);
2529
- collect_style_elements(node.alternate, styles);
2530
- return;
2531
- }
2532
-
2533
- if (is_switch_control_node(node)) {
2534
- for (const switch_case of node.cases || []) {
2535
- collect_style_elements(switch_case.consequent || [], styles);
2536
- }
2537
- return;
2538
- }
2539
-
2540
- if (is_try_control_node(node)) {
2541
- collect_style_elements(node.block, styles);
2542
- collect_style_elements(node.handler?.body, styles);
2543
- collect_style_elements(node.finalizer, styles);
2544
- }
2545
- }
2546
-
2547
- /**
2548
- * @template {AST.NativeTSRXNode} T
2549
- * @param {T} node
2550
- * @param {string} hash
2551
- * @param {'class' | 'className'} jsx_class_attr_name
2552
- * @param {boolean} preserve_style_elements
2553
- * @returns {T}
2554
- */
2555
- function annotate_tsrx_with_hash(node, hash, jsx_class_attr_name, preserve_style_elements) {
2556
- // `annotate_with_hash` returns null for a `<style>` element it drops, so
2557
- // filter before the children go back on the node.
2558
- let children = node_children(node)
2559
- .map((statement) =>
2560
- annotate_with_hash(
2561
- clone_ast_node(statement),
2562
- hash,
2563
- jsx_class_attr_name,
2564
- preserve_style_elements,
2565
- ),
2566
- )
2567
- .filter(is_ast_node);
2568
- if (!preserve_style_elements) {
2569
- children = strip_style_elements_from_list(children);
2570
- }
2571
- // Shallow copy with rewritten children; the spread preserves `node`'s type.
2572
- return /** @type {T} */ ({ ...node, children });
2573
- }
2574
-
2575
- /**
2576
- * Drop `<style>` elements from a child list, recursing into the nodes that
2577
- * survive.
2578
- *
2579
- * @param {Array<AST.Node | null | undefined>} nodes
2580
- * @returns {AST.Node[]}
2581
- */
2582
- function strip_style_elements_from_list(nodes) {
2583
- /** @type {AST.Node[]} */
2584
- const kept = [];
2585
- for (const child of nodes) {
2586
- if (is_style_element(child)) continue;
2587
- const stripped = strip_style_elements(child);
2588
- if (stripped) kept.push(stripped);
2589
- }
2590
- return kept;
2591
- }
2592
-
2593
- /**
2594
- * Strip `<style>` elements out of one node's render subtree, in place. Returns
2595
- * `null` when the node itself is a `<style>` element and should be dropped, or
2596
- * when there is no node — child lists can hold holes.
2597
- *
2598
- * @param {AST.Node | null | undefined} node
2599
- * @returns {AST.Node | null}
2600
- */
2601
- function strip_style_elements(node) {
2602
- if (!node || is_style_element(node)) {
2603
- return null;
2604
- }
2605
-
2606
- if (is_function_or_class_boundary(node)) {
2607
- return node;
2608
- }
2609
-
2610
- if ((node.type === 'JSXElement' || node.type === 'JSXFragment') && node.metadata?.native_tsrx) {
2611
- node.children = strip_style_elements_from_list(node.children || []);
2612
- return node;
2613
- }
2614
-
2615
- if (node.type === 'BlockStatement') {
2616
- node.body = /** @type {AST.Statement[]} */ (strip_style_elements_from_list(node.body || []));
2617
- return node;
2618
- }
2619
-
2620
- if (is_if_control_node(node)) {
2621
- const consequent = strip_style_elements(node.consequent);
2622
- if (consequent) node.consequent = /** @type {AST.Statement} */ (consequent);
2623
- if (node.alternate) {
2624
- const alternate = strip_style_elements(node.alternate);
2625
- if (alternate) node.alternate = /** @type {AST.Statement} */ (alternate);
2626
- }
2627
- return node;
2628
- }
2629
-
2630
- if (is_switch_control_node(node)) {
2631
- for (const switch_case of node.cases || []) {
2632
- switch_case.consequent = /** @type {AST.Statement[]} */ (
2633
- strip_style_elements_from_list(switch_case.consequent || [])
2634
- );
2635
- }
2636
- return node;
2637
- }
2638
-
2639
- if (is_try_control_node(node)) {
2640
- strip_style_elements(node.block);
2641
- if (node.handler?.body) strip_style_elements(node.handler.body);
2642
- if (node.finalizer) strip_style_elements(node.finalizer);
2643
- }
2644
-
2645
- return node;
2646
- }
2647
-
2648
2313
  /**
2649
2314
  * @param {AST.Node[]} path
2650
2315
  * @returns {boolean}
@@ -2708,15 +2373,28 @@ function create_native_tsrx_statement_list_block(block, transform_context) {
2708
2373
  * @returns {AST.Statement[]}
2709
2374
  */
2710
2375
  function create_native_tsrx_render_statements(fragment, transform_context) {
2711
- return with_tsrx_fragment_styles(fragment, transform_context, (style_context) => {
2712
- const target = style_context?.fragment ?? fragment;
2713
- const render_nodes =
2714
- target.type === 'JSXFragment' ? get_tsrx_render_children(target) : [target];
2715
- return [
2716
- ...create_tsrx_style_ref_setup_statements(target, style_context, transform_context),
2717
- ...build_render_statements(render_nodes, true, transform_context, fragment),
2718
- ];
2719
- });
2376
+ const render_nodes =
2377
+ fragment.type === 'JSXFragment' ? get_tsrx_render_children(fragment) : [fragment];
2378
+ return [
2379
+ ...take_style_ref_statements(fragment),
2380
+ ...build_render_statements(render_nodes, true, transform_context, fragment),
2381
+ ];
2382
+ }
2383
+
2384
+ /**
2385
+ * Style `ref` setup statements the scope pre-pass left on a scope rooted at
2386
+ * a native fragment/element (see style-scopes.js), consumed once: the same
2387
+ * fragment can reach both the return-statement lowering and the fragment
2388
+ * visitor.
2389
+ *
2390
+ * @param {AST.Node} fragment
2391
+ * @returns {AST.Statement[]}
2392
+ */
2393
+ function take_style_ref_statements(fragment) {
2394
+ const statements = fragment.metadata?.tsrx_style_ref_statements;
2395
+ if (!statements) return [];
2396
+ delete fragment.metadata.tsrx_style_ref_statements;
2397
+ return statements;
2720
2398
  }
2721
2399
 
2722
2400
  /**
@@ -3909,20 +3587,27 @@ function to_jsx_element(
3909
3587
  ? opening_element_node
3910
3588
  : set_loc(opening_element_node, node.openingElement || node);
3911
3589
 
3912
- const closingElement = selfClosing
3913
- ? null
3914
- : set_loc(
3590
+ let closingElement = null;
3591
+ if (!selfClosing) {
3592
+ const authored_closing = node.closingElement;
3593
+ if (authored_closing) {
3594
+ // Clone from the actual closing name when there is one: a dynamic
3595
+ // tag's closing expression (`</{Tag}>`) has its own source positions,
3596
+ // which editor mappings need.
3597
+ closingElement = set_loc(
3915
3598
  b.jsx_closing_element(
3916
- // Clone from the actual closing name when there is one: a dynamic
3917
- // tag's closing expression (`</{Tag}>`) has its own source positions,
3918
- // which editor mappings need.
3919
- clone_jsx_name(
3920
- node.closingElement?.name ?? name,
3921
- node.closingElement?.name || node.closingElement || node,
3922
- ),
3599
+ clone_jsx_name(authored_closing.name ?? name, authored_closing.name || authored_closing),
3923
3600
  ),
3924
- node.closingElement || node,
3601
+ authored_closing,
3925
3602
  );
3603
+ } else {
3604
+ // Recovered unclosed tags have no authored close. Map the synthesized
3605
+ // name to the opening name token so rename/hover land on `span`, not
3606
+ // `<spa`, and leave the closing element itself unlocated so it does
3607
+ // not steal the opening `<` mapping.
3608
+ closingElement = b.jsx_closing_element(clone_jsx_name(name, source_opening.name));
3609
+ }
3610
+ }
3926
3611
 
3927
3612
  const element = set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
3928
3613
  if (node.metadata?.dynamicElement === true) {
@@ -4448,16 +4133,8 @@ function get_jsx_code_block_body_nodes(node, transform_context) {
4448
4133
  return node.body || [];
4449
4134
  }
4450
4135
 
4451
- if (is_native_tsrx_node(node.render)) {
4452
- const style_context = prepare_tsrx_fragment_styles(node.render, transform_context);
4453
- const render = style_context?.fragment ?? node.render;
4454
- return [
4455
- ...(node.body || []),
4456
- ...create_tsrx_style_ref_setup_statements(render, style_context, transform_context),
4457
- render,
4458
- ];
4459
- }
4460
-
4136
+ // Scoped styles were collected, stamped, and stripped by the pre-pass;
4137
+ // style `ref` setup statements already sit at the end of `body`.
4461
4138
  return [...(node.body || []), node.render];
4462
4139
  }
4463
4140