@tsrx/core 0.1.8 → 0.1.10

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.
@@ -5,4 +5,5 @@ export const DIAGNOSTIC_CODES = {
5
5
  UNCLOSED_TAG: 'tsrx-unclosed-tag',
6
6
  MISMATCHED_CLOSING_TAG: 'tsrx-mismatched-closing-tag',
7
7
  TEMPLATE_EXPRESSION_TRAILING_SEMICOLON: 'tsrx-template-expression-trailing-semicolon',
8
+ HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE: 'tsrx-html-directive-as-attribute-value',
8
9
  };
package/src/index.js CHANGED
@@ -146,14 +146,21 @@ export {
146
146
  clone_switch_helper_invocation as cloneSwitchHelperInvocation,
147
147
  collect_param_bindings as collectParamBindings,
148
148
  collect_statement_bindings as collectStatementBindings,
149
+ create_host_html_attribute as createHostHtmlAttribute,
150
+ create_host_html_conflict_error as createHostHtmlConflictError,
149
151
  createJsxTransform,
150
152
  CREATE_REF_PROP_INTERNAL_NAME,
151
153
  extract_jsx_setup_declarations as extractJsxSetupDeclarations,
154
+ get_host_html_conflicting_attribute as getHostHtmlConflictingAttribute,
155
+ get_invalid_html_child_error_message as getInvalidHtmlChildErrorMessage,
156
+ is_component_like_element,
152
157
  is_ref_prop_expression as isRefPropExpression,
153
158
  MERGE_REFS_INTERNAL_NAME,
154
159
  merge_duplicate_refs as mergeDuplicateRefs,
155
160
  NORMALIZE_SPREAD_PROPS_INTERNAL_NAME,
156
161
  plan_switch_lift as planSwitchLift,
162
+ recover_invalid_html_child as recoverInvalidHtmlChild,
163
+ rewrite_host_html_children as rewriteHostHtmlChildren,
157
164
  return_value_body_to_expression as returnValueBodyToExpression,
158
165
  rewrite_loop_continues_to_bare_returns as rewriteLoopContinuesToBareReturns,
159
166
  to_jsx_attribute as toJsxAttribute,
@@ -229,6 +236,7 @@ export {
229
236
 
230
237
  // Analyze
231
238
  export { analyze_css as analyzeCss } from './analyze/css-analyze.js';
239
+ export { prune_css as pruneCss } from './analyze/prune.js';
232
240
  export {
233
241
  CLASS_COMPONENT_AS_NON_ARROW_PROPERTY_ERROR,
234
242
  COMPONENT_DO_WHILE_STATEMENT_ERROR,
package/src/plugin.js CHANGED
@@ -19,6 +19,8 @@ import { DIAGNOSTIC_CODES } from './diagnostics.js';
19
19
 
20
20
  const JSX_EXPRESSION_VALUE_ERROR =
21
21
  'JSX elements cannot be used as expressions. Wrap JSX with `<>...</>` or `<tsx>...</tsx>`, wrap TSRX templates with `<tsrx>...</tsrx>`, or use elements as statements within a component.';
22
+ const HTML_ATTRIBUTE_VALUE_ERROR =
23
+ '`{html ...}` is not supported as an attribute value. Use a string literal or expression without `html`.';
22
24
 
23
25
  const CharCode = Object.freeze({
24
26
  tab: 9,
@@ -1969,6 +1971,27 @@ export function TSRXPlugin(config) {
1969
1971
  /** @type {AST.RefAttribute} */ (node).argument = this.parseMaybeAssign();
1970
1972
  this.expect(tt.braceR);
1971
1973
  return /** @type {AST.RefAttribute} */ (this.finishNode(node, 'RefAttribute'));
1974
+ } else if (this.type === tt.name && this.value === 'html') {
1975
+ // {html ...}
1976
+ // The support is purely for better error messages to avoid
1977
+ // the parser throw an unexpected token error
1978
+ const id = /** @type {AST.Identifier} */ (this.parseIdentNode());
1979
+ id.tracked = false;
1980
+ this.finishNode(id, 'Identifier');
1981
+ this.next();
1982
+ const value = this.type === tt.braceR ? id : this.parseMaybeAssign();
1983
+ const report_end = this.type === tt.braceR ? this.end : (value.end ?? this.end);
1984
+ this.#report_recoverable_error_range(
1985
+ node.start ?? id.start ?? this.start,
1986
+ report_end,
1987
+ HTML_ATTRIBUTE_VALUE_ERROR,
1988
+ DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
1989
+ );
1990
+ /** @type {AST.Attribute} */ (node).name = id;
1991
+ /** @type {AST.Attribute} */ (node).value = value;
1992
+ /** @type {AST.Attribute} */ (node).shorthand = false;
1993
+ this.expect(tt.braceR);
1994
+ return this.finishNode(node, 'Attribute');
1972
1995
  } else if (this.type === tt.ellipsis) {
1973
1996
  this.expect(tt.ellipsis);
1974
1997
  /** @type {AST.SpreadAttribute} */ (node).argument = this.parseMaybeAssign();
@@ -1992,10 +2015,18 @@ export function TSRXPlugin(config) {
1992
2015
  }
1993
2016
  }
1994
2017
  /** @type {ESTreeJSX.JSXAttribute} */ (node).name = this.jsx_parseNamespacedName();
1995
- /** @type {ESTreeJSX.JSXAttribute} */ (node).value =
1996
- /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
1997
- this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
2018
+ const value = /** @type {ESTreeJSX.JSXAttribute['value'] | null} */ (
2019
+ this.eat(tt.eq) ? this.jsx_parseAttributeValue() : null
2020
+ );
2021
+ if (value?.type === 'JSXExpressionContainer' && value.html) {
2022
+ this.#report_recoverable_error_range(
2023
+ value.start ?? node.start ?? this.start,
2024
+ value.end ?? node.end ?? this.end,
2025
+ HTML_ATTRIBUTE_VALUE_ERROR,
2026
+ DIAGNOSTIC_CODES.HTML_DIRECTIVE_AS_ATTRIBUTE_VALUE,
1998
2027
  );
2028
+ }
2029
+ /** @type {ESTreeJSX.JSXAttribute} */ (node).value = value;
1999
2030
  return this.finishNode(node, 'JSXAttribute');
2000
2031
  }
2001
2032
 
@@ -2205,6 +2236,8 @@ export function TSRXPlugin(config) {
2205
2236
  // In JSX text mode, '<' and '{' always start a tag/expression container.
2206
2237
  // `exprAllowed` can be false here due to surrounding parser state, but
2207
2238
  // throwing breaks valid templates (e.g. sibling tags after a close).
2239
+ this.start = this.pos;
2240
+ this.startLoc = this.curPosition();
2208
2241
  if (ch === CharCode.lessThan) {
2209
2242
  ++this.pos;
2210
2243
  return this.finishToken(tstt.jsxTagStart);
@@ -2434,6 +2467,8 @@ export function TSRXPlugin(config) {
2434
2467
  /** @type {AST.NodeWithLocation} */ (element).loc.start = position;
2435
2468
  element.metadata = { path: [] };
2436
2469
  element.children = [];
2470
+ element.type = 'Element';
2471
+ this.#path.push(element);
2437
2472
 
2438
2473
  const open = /** @type {ESTreeJSX.JSXOpeningElement & AST.NodeWithLocation} */ (
2439
2474
  this.jsx_parseOpeningElementAt(start, position)
@@ -2492,8 +2527,6 @@ export function TSRXPlugin(config) {
2492
2527
  element.type = 'Element';
2493
2528
  }
2494
2529
 
2495
- this.#path.push(element);
2496
-
2497
2530
  for (const attr of open.attributes) {
2498
2531
  if (attr.type === 'JSXAttribute') {
2499
2532
  /** @type {AST.Attribute} */ (/** @type {unknown} */ (attr)).type = 'Attribute';
@@ -2534,7 +2567,12 @@ export function TSRXPlugin(config) {
2534
2567
 
2535
2568
  element.attributes = open.attributes;
2536
2569
  element.metadata ??= { path: [] };
2537
- element.metadata.commentContainerId = ++this.#commentContextId;
2570
+ // Opening-tag parsing can tokenize comments that appear before the first
2571
+ // child. Preserve that early container id so the comment stays associated
2572
+ // with this element during comment attachment/printing.
2573
+ if (element.metadata.commentContainerId === undefined) {
2574
+ element.metadata.commentContainerId = ++this.#commentContextId;
2575
+ }
2538
2576
 
2539
2577
  if (element.selfClosing) {
2540
2578
  this.#path.pop();
@@ -2548,7 +2586,7 @@ export function TSRXPlugin(config) {
2548
2586
  enterScope: true,
2549
2587
  });
2550
2588
 
2551
- if (element.type === 'Tsx') {
2589
+ if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2552
2590
  this.#path.pop();
2553
2591
 
2554
2592
  if (!element.unclosed) {
@@ -2725,7 +2763,7 @@ export function TSRXPlugin(config) {
2725
2763
  enterScope: true,
2726
2764
  });
2727
2765
 
2728
- if (element.type === 'Tsx') {
2766
+ if (/** @type {AST.Tsx} */ (element).type === 'Tsx') {
2729
2767
  this.#path.pop();
2730
2768
 
2731
2769
  if (!element.unclosed) {
@@ -2749,12 +2787,15 @@ export function TSRXPlugin(config) {
2749
2787
  this.#popTsxTokenContextBeforeTemplateExpressionChild();
2750
2788
  this.next();
2751
2789
  }
2752
- } else if (element.type === 'TsxCompat') {
2790
+ } else if (/** @type {AST.TsxCompat} */ (element).type === 'TsxCompat') {
2753
2791
  this.#path.pop();
2754
2792
 
2755
2793
  if (!element.unclosed) {
2756
2794
  const raise_error = () => {
2757
- this.raise(this.start, `Expected closing tag '</tsx:${element.kind}>'`);
2795
+ this.raise(
2796
+ this.start,
2797
+ `Expected closing tag '</tsx:${/** @type {AST.TsxCompat} */ (element).kind}>'`,
2798
+ );
2758
2799
  };
2759
2800
 
2760
2801
  this.next();
@@ -2771,7 +2812,7 @@ export function TSRXPlugin(config) {
2771
2812
  raise_error();
2772
2813
  }
2773
2814
  this.next();
2774
- if (this.value !== element.kind) {
2815
+ if (this.value !== /** @type {AST.TsxCompat} */ (element).kind) {
2775
2816
  raise_error();
2776
2817
  }
2777
2818
  this.next();
@@ -2781,7 +2822,10 @@ export function TSRXPlugin(config) {
2781
2822
  this.#popTsxTokenContextBeforeTemplateExpressionChild();
2782
2823
  this.next();
2783
2824
  }
2784
- } else if (element.type === 'Tsrx' && this.#path[this.#path.length - 1] === element) {
2825
+ } else if (
2826
+ /** @type {AST.Tsrx} */ (element).type === 'Tsrx' &&
2827
+ this.#path[this.#path.length - 1] === element
2828
+ ) {
2785
2829
  this.#report_broken_markup_error(
2786
2830
  this.start,
2787
2831
  "Unclosed tag '<tsrx>'. Expected '</tsrx>' before end of component.",
@@ -5,6 +5,8 @@
5
5
  import { walk } from 'zimmerframe';
6
6
  import { print } from 'esrap';
7
7
  import { error } from '../../errors.js';
8
+ import { analyze_css } from '../../analyze/css-analyze.js';
9
+ import { prune_css } from '../../analyze/prune.js';
8
10
  import {
9
11
  ensure_function_metadata,
10
12
  in_jsx_child_context,
@@ -36,7 +38,11 @@ import {
36
38
  import * as b from '../../utils/builders.js';
37
39
  import { apply_lazy_transforms, preallocate_lazy_ids } from '../lazy.js';
38
40
  import { find_first_top_level_await_in_component_body } from '../await.js';
39
- import { prepare_stylesheet_for_render, annotate_component_with_hash } from '../scoping.js';
41
+ import {
42
+ prepare_stylesheet_for_render,
43
+ annotate_component_with_hash,
44
+ is_style_element,
45
+ } from '../scoping.js';
40
46
  import {
41
47
  validate_class_component_declarations,
42
48
  validate_component_loop_break_statement,
@@ -60,20 +66,43 @@ const HOOK_CALLBACK_OUTER_MUTATION_ERROR =
60
66
  const TEMPLATE_FRAGMENT_ERROR =
61
67
  'JSX fragment syntax is not needed in TSRX templates. TSRX renders in immediate mode, so everything is already a fragment. Use `<>...</>` only within <tsx>...</tsx>.';
62
68
 
69
+ /**
70
+ * @param {TransformContext} transform_context
71
+ * @returns {string}
72
+ */
73
+ export function get_invalid_html_child_error_message(transform_context) {
74
+ return `\`{html ...}\` is only supported as the sole child of an element in ${transform_context.platform.name}.`;
75
+ }
76
+
63
77
  /**
64
78
  * @param {AST.Node} node
65
79
  * @param {TransformContext} transform_context
66
80
  */
67
- function report_html_template_unsupported_error(node, transform_context) {
68
- // this should be a fatal error so we don't pass the errors collection,
69
- // since we don't have a transform for the Html node
81
+ function report_invalid_html_child_error(node, transform_context) {
70
82
  error(
71
- `\`{html ...}\` is not supported on the ${transform_context.platform.name} target. Use \`dangerouslySetInnerHTML={{ __html: ... }}\` as an element attribute instead.`,
83
+ get_invalid_html_child_error_message(transform_context),
72
84
  transform_context.filename,
73
85
  node,
86
+ transform_context.errors,
87
+ transform_context.comments,
74
88
  );
75
89
  }
76
90
 
91
+ /**
92
+ * In loose/editor mode `error(...)` records the diagnostic and continues, so an
93
+ * invalid standalone `{html ...}` child still needs a valid expression node for
94
+ * the virtual TSX output.
95
+ *
96
+ * @param {any} node
97
+ * @param {TransformContext} transform_context
98
+ * @returns {ESTreeJSX.JSXExpressionContainer}
99
+ */
100
+ export function recover_invalid_html_child(node, transform_context) {
101
+ report_invalid_html_child_error(node, transform_context);
102
+ const expression = set_loc(clone_expression_node(node.expression), node);
103
+ return to_jsx_expression_container(expression, node);
104
+ }
105
+
77
106
  /**
78
107
  * @param {AST.Node} node
79
108
  * @param {TransformContext} transform_context
@@ -403,12 +432,14 @@ export function createJsxTransform(platform) {
403
432
 
404
433
  const css = as_any.css;
405
434
  if (css) {
435
+ apply_css_definition_metadata(as_any, css);
406
436
  stylesheets.push(css);
407
437
  const hash = css.hash;
408
438
  annotate_component_with_hash(
409
439
  as_any,
410
440
  hash,
411
441
  platform.jsx.rewriteClassAttr ? 'className' : 'class',
442
+ transform_context.typeOnly,
412
443
  );
413
444
  }
414
445
  return next(state);
@@ -586,6 +617,73 @@ export function createJsxTransform(platform) {
586
617
  return transform;
587
618
  }
588
619
 
620
+ /**
621
+ * Attach selector-location metadata used by editor definitions/hover before
622
+ * the shared scoping pass mutates class attributes with the component hash.
623
+ *
624
+ * @param {any} component
625
+ * @param {any} css
626
+ * @returns {void}
627
+ */
628
+ function apply_css_definition_metadata(component, css) {
629
+ analyze_css(css);
630
+
631
+ const metadata = component.metadata || (component.metadata = { path: [] });
632
+ const style_classes = metadata.styleClasses || (metadata.styleClasses = new Map());
633
+ const top_scoped_classes = metadata.topScopedClasses || new Map();
634
+ const elements = collect_css_prunable_elements(component.body || []);
635
+
636
+ for (const element of elements) {
637
+ prune_css(css, element, style_classes, top_scoped_classes);
638
+ }
639
+
640
+ if (top_scoped_classes.size > 0) {
641
+ metadata.topScopedClasses = top_scoped_classes;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * @param {any} value
647
+ * @param {any[]} [elements]
648
+ * @returns {any[]}
649
+ */
650
+ function collect_css_prunable_elements(value, elements = []) {
651
+ if (!value || typeof value !== 'object') {
652
+ return elements;
653
+ }
654
+
655
+ if (Array.isArray(value)) {
656
+ for (const child of value) {
657
+ collect_css_prunable_elements(child, elements);
658
+ }
659
+ return elements;
660
+ }
661
+
662
+ if (
663
+ value.type === 'FunctionDeclaration' ||
664
+ value.type === 'FunctionExpression' ||
665
+ value.type === 'ArrowFunctionExpression' ||
666
+ value.type === 'Component'
667
+ ) {
668
+ return elements;
669
+ }
670
+
671
+ if (value.type === 'Element') {
672
+ if (!is_style_element(value)) {
673
+ elements.push(value);
674
+ }
675
+ }
676
+
677
+ for (const key of Object.keys(value)) {
678
+ if (key === 'loc' || key === 'start' || key === 'end' || key === 'metadata' || key === 'css') {
679
+ continue;
680
+ }
681
+ collect_css_prunable_elements(value[key], elements);
682
+ }
683
+
684
+ return elements;
685
+ }
686
+
589
687
  /**
590
688
  * Detect a top-level `"use server"` directive. Used by platforms whose
591
689
  * validation rule requires the directive to enable top-level `await`
@@ -2310,10 +2408,19 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
2310
2408
  selfClosing = child_transform.selfClosing;
2311
2409
  }
2312
2410
  } else {
2313
- if (walked_children.some((/** @type {any} */ c) => c && c.type === 'Html')) {
2314
- return report_html_template_unsupported_error(node, transform_context);
2411
+ const html_child_transform = rewrite_host_html_children(
2412
+ node,
2413
+ walked_children,
2414
+ raw_children,
2415
+ attributes,
2416
+ transform_context,
2417
+ );
2418
+ if (html_child_transform) {
2419
+ children = html_child_transform.children;
2420
+ selfClosing = html_child_transform.selfClosing;
2421
+ } else {
2422
+ children = create_element_children(walked_children, transform_context);
2315
2423
  }
2316
- children = create_element_children(walked_children, transform_context);
2317
2424
  }
2318
2425
  const has_unmappable_attribute = attributes.some(
2319
2426
  (/** @type {any} */ attribute) => attribute?.metadata?.has_unmappable_value,
@@ -2341,6 +2448,117 @@ function to_jsx_element(node, transform_context, raw_children = node.children ||
2341
2448
  return set_loc(b.jsx_element_fresh(openingElement, closingElement, children), node);
2342
2449
  }
2343
2450
 
2451
+ /**
2452
+ * @param {any} node
2453
+ * @param {any[]} walked_children
2454
+ * @param {any[]} raw_children
2455
+ * @param {any[]} attributes
2456
+ * @param {TransformContext} transform_context
2457
+ * @returns {{ children: any[]; selfClosing: boolean } | null}
2458
+ */
2459
+ export function rewrite_host_html_children(
2460
+ node,
2461
+ walked_children,
2462
+ raw_children,
2463
+ attributes,
2464
+ transform_context,
2465
+ ) {
2466
+ const source_children = raw_children || walked_children;
2467
+ const source_html_index = source_children.findIndex((child) => child?.type === 'Html');
2468
+ if (source_html_index === -1) {
2469
+ return null;
2470
+ }
2471
+ const source_html = source_children[source_html_index];
2472
+ const walked_html =
2473
+ walked_children[source_html_index]?.type === 'Html'
2474
+ ? walked_children[source_html_index]
2475
+ : source_html;
2476
+
2477
+ if (is_component_like_element(node) || source_children.length !== 1) {
2478
+ report_invalid_html_child_error(source_html, transform_context);
2479
+ }
2480
+
2481
+ const conflicting_attribute = get_host_html_conflicting_attribute(attributes, transform_context);
2482
+ if (conflicting_attribute !== null) {
2483
+ error(
2484
+ create_host_html_conflict_error(conflicting_attribute, transform_context),
2485
+ transform_context.filename,
2486
+ source_html,
2487
+ transform_context.errors,
2488
+ transform_context.comments,
2489
+ );
2490
+ }
2491
+
2492
+ attributes.push(create_host_html_attribute(walked_html, source_html, transform_context));
2493
+
2494
+ return { children: [], selfClosing: true };
2495
+ }
2496
+
2497
+ /**
2498
+ * @param {any[]} attributes
2499
+ * @param {TransformContext} transform_context
2500
+ * @returns {{ kind: 'attribute'; name: string } | null}
2501
+ */
2502
+ export function get_host_html_conflicting_attribute(attributes, transform_context) {
2503
+ const conflicting_attributes = get_host_html_conflicting_attribute_names(transform_context);
2504
+ for (const name of conflicting_attributes) {
2505
+ if (has_jsx_attribute(attributes, name)) {
2506
+ return { kind: 'attribute', name };
2507
+ }
2508
+ }
2509
+
2510
+ return null;
2511
+ }
2512
+
2513
+ /**
2514
+ * @param {{ kind: 'attribute'; name: string }} conflicting_attribute
2515
+ * @param {TransformContext} transform_context
2516
+ * @returns {string}
2517
+ */
2518
+ export function create_host_html_conflict_error(conflicting_attribute, transform_context) {
2519
+ const html_attribute = get_host_html_attribute_name(transform_context);
2520
+ return `\`{html ...}\` lowers to \`${html_attribute}\` on the ${transform_context.platform.name} target and cannot be combined with an existing \`${conflicting_attribute.name}\` attribute.`;
2521
+ }
2522
+
2523
+ /**
2524
+ * @param {TransformContext} transform_context
2525
+ * @returns {string[]}
2526
+ */
2527
+ function get_host_html_conflicting_attribute_names(transform_context) {
2528
+ switch (transform_context.platform.name) {
2529
+ case 'Solid':
2530
+ return ['innerHTML', 'textContent'];
2531
+ case 'Vue':
2532
+ return ['innerHTML'];
2533
+ default:
2534
+ return [get_host_html_attribute_name(transform_context)];
2535
+ }
2536
+ }
2537
+
2538
+ /**
2539
+ * @param {TransformContext} transform_context
2540
+ * @returns {'dangerouslySetInnerHTML' | 'innerHTML'}
2541
+ */
2542
+ function get_host_html_attribute_name(transform_context) {
2543
+ return transform_context.platform.jsx?.htmlProp === 'dangerouslySetInnerHTML'
2544
+ ? 'dangerouslySetInnerHTML'
2545
+ : 'innerHTML';
2546
+ }
2547
+
2548
+ /**
2549
+ * @param {any[]} attributes
2550
+ * @param {string} name
2551
+ * @returns {boolean}
2552
+ */
2553
+ function has_jsx_attribute(attributes, name) {
2554
+ return attributes.some(
2555
+ (attr) =>
2556
+ attr?.type === 'JSXAttribute' &&
2557
+ attr.name?.type === 'JSXIdentifier' &&
2558
+ attr.name.name === name,
2559
+ );
2560
+ }
2561
+
2344
2562
  /**
2345
2563
  * @param {any[]} children
2346
2564
  * @param {TransformContext} transform_context
@@ -3580,7 +3798,7 @@ function to_jsx_child(node, transform_context) {
3580
3798
  case 'TSRXExpression':
3581
3799
  return to_jsx_expression_container(node.expression, node);
3582
3800
  case 'Html':
3583
- return report_html_template_unsupported_error(node, transform_context);
3801
+ return recover_invalid_html_child(node, transform_context);
3584
3802
  case 'IfStatement':
3585
3803
  return (
3586
3804
  transform_context.platform.hooks?.controlFlow?.ifStatement ?? if_statement_to_jsx_child
@@ -4918,7 +5136,7 @@ function transform_element_attributes_dispatch(attrs, transform_context, element
4918
5136
  * @param {any} element
4919
5137
  * @returns {boolean}
4920
5138
  */
4921
- function is_component_like_element(element) {
5139
+ export function is_component_like_element(element) {
4922
5140
  const id = element?.id;
4923
5141
  if (!id) return false;
4924
5142
  if (id.type === 'Identifier') return /^[A-Z]/.test(id.name);
@@ -5127,6 +5345,31 @@ function is_named_ref_attribute(attr) {
5127
5345
  );
5128
5346
  }
5129
5347
 
5348
+ /**
5349
+ * @param {any} html_expression
5350
+ * @param {any} source_attr
5351
+ * @param {TransformContext} transform_context
5352
+ * @returns {any}
5353
+ */
5354
+ export function create_host_html_attribute(html_expression, source_attr, transform_context) {
5355
+ const expression =
5356
+ html_expression?.type === 'Html' ? html_expression.expression : html_expression;
5357
+ const name = get_host_html_attribute_name(transform_context);
5358
+ const value =
5359
+ name === 'dangerouslySetInnerHTML'
5360
+ ? set_loc(b.object([b.prop('init', b.id('__html'), expression)]), source_attr)
5361
+ : expression;
5362
+ const value_container = to_jsx_expression_container(value, source_attr);
5363
+ if (name !== 'dangerouslySetInnerHTML') {
5364
+ setLocation(value_container, source_attr, true);
5365
+ }
5366
+
5367
+ return set_loc(
5368
+ build_jsx_attribute(b.jsx_id(name), value_container, false, source_attr),
5369
+ source_attr,
5370
+ );
5371
+ }
5372
+
5130
5373
  /**
5131
5374
  * @param {any} expression
5132
5375
  * @returns {boolean}
@@ -5379,6 +5622,8 @@ export function to_jsx_attribute(attr, transform_context) {
5379
5622
  attr_name.name === 'class'
5380
5623
  ) {
5381
5624
  attr_name = set_loc(b.id('className'), attr.name);
5625
+ attr_name.metadata.source_name = 'class';
5626
+ attr_name.metadata.source_length = 'class'.length;
5382
5627
  }
5383
5628
 
5384
5629
  const name =
@@ -95,9 +95,15 @@ function is_composite_jsx_element(node) {
95
95
  * @param {any} node
96
96
  * @param {string} hash
97
97
  * @param {'class' | 'className'} [jsx_class_attr_name='class']
98
+ * @param {boolean} [preserve_style_elements=false]
98
99
  * @returns {any}
99
100
  */
100
- export function annotate_with_hash(node, hash, jsx_class_attr_name = 'class') {
101
+ export function annotate_with_hash(
102
+ node,
103
+ hash,
104
+ jsx_class_attr_name = 'class',
105
+ preserve_style_elements = false,
106
+ ) {
101
107
  if (!node || typeof node !== 'object') return node;
102
108
  if (
103
109
  node.type === 'Component' ||
@@ -109,13 +115,19 @@ export function annotate_with_hash(node, hash, jsx_class_attr_name = 'class') {
109
115
  }
110
116
 
111
117
  if (node.type === 'Element') {
118
+ if (preserve_style_elements && is_style_element(node)) {
119
+ node.children = [];
120
+ return node;
121
+ }
112
122
  if (!is_style_element(node) && !is_composite_element(node)) {
113
123
  add_hash_class(node, hash);
114
124
  }
115
125
  if (Array.isArray(node.children)) {
116
126
  node.children = node.children
117
- .filter((/** @type {any} */ child) => !is_style_element(child))
118
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash, jsx_class_attr_name));
127
+ .filter((/** @type {any} */ child) => preserve_style_elements || !is_style_element(child))
128
+ .map((/** @type {any} */ child) =>
129
+ annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
130
+ );
119
131
  }
120
132
  return node;
121
133
  }
@@ -126,7 +138,7 @@ export function annotate_with_hash(node, hash, jsx_class_attr_name = 'class') {
126
138
  }
127
139
  if (Array.isArray(node.children)) {
128
140
  node.children = node.children.map((/** @type {any} */ child) =>
129
- annotate_with_hash(child, hash, jsx_class_attr_name),
141
+ annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
130
142
  );
131
143
  }
132
144
  return node;
@@ -140,10 +152,10 @@ export function annotate_with_hash(node, hash, jsx_class_attr_name = 'class') {
140
152
  const value = node[key];
141
153
  if (Array.isArray(value)) {
142
154
  node[key] = value.map((/** @type {any} */ child) =>
143
- annotate_with_hash(child, hash, jsx_class_attr_name),
155
+ annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
144
156
  );
145
157
  } else if (value && typeof value === 'object') {
146
- node[key] = annotate_with_hash(value, hash, jsx_class_attr_name);
158
+ node[key] = annotate_with_hash(value, hash, jsx_class_attr_name, preserve_style_elements);
147
159
  }
148
160
  }
149
161
 
@@ -154,14 +166,22 @@ export function annotate_with_hash(node, hash, jsx_class_attr_name = 'class') {
154
166
  * @param {any} component
155
167
  * @param {string} hash
156
168
  * @param {'class' | 'className'} [jsx_class_attr_name='class']
169
+ * @param {boolean} [preserve_style_elements=false]
157
170
  * @returns {void}
158
171
  */
159
- export function annotate_component_with_hash(component, hash, jsx_class_attr_name = 'class') {
172
+ export function annotate_component_with_hash(
173
+ component,
174
+ hash,
175
+ jsx_class_attr_name = 'class',
176
+ preserve_style_elements = false,
177
+ ) {
160
178
  /** @type {any[]} */
161
179
  const body = component.body;
162
180
  component.body = body
163
- .filter((/** @type {any} */ child) => !is_style_element(child))
164
- .map((/** @type {any} */ child) => annotate_with_hash(child, hash, jsx_class_attr_name));
181
+ .filter((/** @type {any} */ child) => preserve_style_elements || !is_style_element(child))
182
+ .map((/** @type {any} */ child) =>
183
+ annotate_with_hash(child, hash, jsx_class_attr_name, preserve_style_elements),
184
+ );
165
185
  }
166
186
 
167
187
  /**
@@ -198,7 +218,8 @@ export function add_hash_class(element, hash) {
198
218
 
199
219
  if (value.type === 'Literal' && typeof value.value === 'string') {
200
220
  const merged = `${value.value} ${hash}`;
201
- existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
221
+ value.value = merged;
222
+ value.raw = JSON.stringify(merged);
202
223
  return;
203
224
  }
204
225
 
@@ -233,7 +254,11 @@ function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
233
254
  existing.name = {
234
255
  type: 'JSXIdentifier',
235
256
  name: jsx_class_attr_name,
236
- metadata: existing.name.metadata || { path: [] },
257
+ metadata: {
258
+ ...(existing.name.metadata || { path: [] }),
259
+ source_name: existing.name.name,
260
+ source_length: existing.name.name.length,
261
+ },
237
262
  };
238
263
  }
239
264
 
@@ -245,7 +270,8 @@ function add_hash_class_to_jsx_element(element, hash, jsx_class_attr_name) {
245
270
 
246
271
  if (value.type === 'Literal' && typeof value.value === 'string') {
247
272
  const merged = `${value.value} ${hash}`;
248
- existing.value = { type: 'Literal', value: merged, raw: JSON.stringify(merged) };
273
+ value.value = merged;
274
+ value.raw = JSON.stringify(merged);
249
275
  return;
250
276
  }
251
277