@tsrx/react 0.0.1 → 0.0.2

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": "React compiler built on @tsrx/core",
4
4
  "license": "MIT",
5
5
  "author": "Dominic Gannaway",
6
- "version": "0.0.1",
6
+ "version": "0.0.2",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** @import * as AST from 'estree' */
2
+ /** @import { CodeMapping, ParseOptions } from '@tsrx/core/types' */
2
3
 
3
4
  import { createVolarMappingsResult, parseModule } from '@tsrx/core';
4
5
  import { transform } from './transform.js';
@@ -7,10 +8,11 @@ import { transform } from './transform.js';
7
8
  * Parse tsrx-react source code to an ESTree AST.
8
9
  * @param {string} source
9
10
  * @param {string} [filename]
11
+ * @param {ParseOptions} [options]
10
12
  * @returns {AST.Program}
11
13
  */
12
- export function parse(source, filename) {
13
- return parseModule(source, filename);
14
+ export function parse(source, filename, options) {
15
+ return parseModule(source, filename, options);
14
16
  }
15
17
 
16
18
  /**
@@ -32,13 +34,13 @@ export function compile(source, filename) {
32
34
  *
33
35
  * @param {string} source
34
36
  * @param {string} [filename]
37
+ * @param {ParseOptions} [options]
35
38
  * @returns {import('@tsrx/core/types').VolarMappingsResult}
36
39
  */
37
- export function compile_to_volar_mappings(source, filename) {
38
- const ast = parseModule(source, filename);
40
+ export function compile_to_volar_mappings(source, filename, options) {
41
+ const ast = parseModule(source, filename, options);
39
42
  const transformed = transform(ast, source, filename);
40
-
41
- return createVolarMappingsResult({
43
+ const result = createVolarMappingsResult({
42
44
  ast: transformed.ast,
43
45
  ast_from_source: ast,
44
46
  source,
@@ -46,4 +48,59 @@ export function compile_to_volar_mappings(source, filename) {
46
48
  source_map: transformed.map,
47
49
  errors: [],
48
50
  });
51
+
52
+ return {
53
+ ...result,
54
+ mappings: dedupe_mappings(result.mappings),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Remove byte-for-byte duplicate mappings. React helper extraction can emit
60
+ * identical mapping entries for the same source and generated span, which
61
+ * causes Volar to merge duplicate hover/navigation results.
62
+ *
63
+ * @param {CodeMapping[]} mappings
64
+ * @returns {CodeMapping[]}
65
+ */
66
+ function dedupe_mappings(mappings) {
67
+ const deduped = [];
68
+ const seen = new Set();
69
+
70
+ for (const mapping of mappings) {
71
+ const key = JSON.stringify(serialize_mapping_value(mapping));
72
+
73
+ if (seen.has(key)) {
74
+ continue;
75
+ }
76
+
77
+ seen.add(key);
78
+ deduped.push(mapping);
79
+ }
80
+
81
+ return deduped;
82
+ }
83
+
84
+ /**
85
+ * @param {unknown} value
86
+ * @returns {unknown}
87
+ */
88
+ function serialize_mapping_value(value) {
89
+ if (typeof value === 'function') {
90
+ return value.toString();
91
+ }
92
+
93
+ if (Array.isArray(value)) {
94
+ return value.map(serialize_mapping_value);
95
+ }
96
+
97
+ if (value && typeof value === 'object') {
98
+ return Object.fromEntries(
99
+ Object.entries(value)
100
+ .sort(([left], [right]) => left.localeCompare(right))
101
+ .map(([key, nested_value]) => [key, serialize_mapping_value(nested_value)]),
102
+ );
103
+ }
104
+
105
+ return value;
49
106
  }
package/src/transform.js CHANGED
@@ -77,7 +77,9 @@ export function transform(ast, source, filename) {
77
77
 
78
78
  Text(node, { next }) {
79
79
  const inner = /** @type {any} */ (next() ?? node);
80
- return /** @type {any} */ (to_jsx_expression_container(inner.expression, inner));
80
+ return /** @type {any} */ (
81
+ to_jsx_expression_container(to_text_expression(inner.expression, inner), inner)
82
+ );
81
83
  },
82
84
 
83
85
  TSRXExpression(node, { next }) {
@@ -133,12 +135,18 @@ function component_to_function_declaration(component, transform_context) {
133
135
  metadata: {
134
136
  path: [],
135
137
  is_component: true,
136
- is_method: true,
137
138
  },
138
139
  });
139
140
 
140
141
  fn.metadata.generated_helpers = helper_state.helpers;
141
142
 
143
+ if (fn.id) {
144
+ fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
145
+ ...fn.id.metadata,
146
+ is_component: true,
147
+ });
148
+ }
149
+
142
150
  setLocation(fn, /** @type {any} */ (component), true);
143
151
  return fn;
144
152
  }
@@ -460,10 +468,16 @@ function create_helper_function_declaration(
460
468
  metadata: {
461
469
  path: [],
462
470
  is_component: true,
463
- is_method: true,
464
471
  },
465
472
  });
466
473
 
474
+ if (fn.id) {
475
+ fn.id.metadata = /** @type {AST.Identifier['metadata']} */ ({
476
+ ...fn.id.metadata,
477
+ is_component: true,
478
+ });
479
+ }
480
+
467
481
  return set_loc(fn, source_node);
468
482
  }
469
483
 
@@ -799,6 +813,22 @@ function is_style_element(node) {
799
813
  );
800
814
  }
801
815
 
816
+ /**
817
+ * @param {any} node
818
+ * @returns {boolean}
819
+ */
820
+ function is_composite_element(node) {
821
+ if (!node || node.type !== 'Element' || !node.id) {
822
+ return false;
823
+ }
824
+
825
+ if (node.id.type === 'Identifier') {
826
+ return /^[A-Z]/.test(node.id.name);
827
+ }
828
+
829
+ return node.id.type === 'MemberExpression';
830
+ }
831
+
802
832
  /**
803
833
  * Recursively walk Element nodes within a component body and add the hash
804
834
  * class name so scope-qualified selectors (e.g. `.foo.hash`) match.
@@ -819,7 +849,7 @@ function annotate_with_hash(node, hash) {
819
849
  }
820
850
 
821
851
  if (node.type === 'Element') {
822
- if (!is_style_element(node)) {
852
+ if (!is_style_element(node) && !is_composite_element(node)) {
823
853
  add_hash_class(node, hash);
824
854
  }
825
855
  if (Array.isArray(node.children)) {
@@ -1274,6 +1304,7 @@ function to_jsx_child(node, transform_context) {
1274
1304
  case 'Element':
1275
1305
  return to_jsx_element(node, transform_context);
1276
1306
  case 'Text':
1307
+ return to_jsx_expression_container(to_text_expression(node.expression, node), node);
1277
1308
  case 'TSRXExpression':
1278
1309
  return to_jsx_expression_container(node.expression, node);
1279
1310
  case 'IfStatement':
@@ -1826,6 +1857,54 @@ function to_jsx_expression_container(expression, source_node = expression) {
1826
1857
  });
1827
1858
  }
1828
1859
 
1860
+ /**
1861
+ * Ripple's `{text expr}` always renders text, even for booleans and objects.
1862
+ * React's normal `{expr}` child semantics would drop booleans and render
1863
+ * elements as elements, so we coerce to a text value explicitly.
1864
+ * @param {AST.Expression} expression
1865
+ * @param {any} [source_node]
1866
+ * @returns {AST.Expression}
1867
+ */
1868
+ function to_text_expression(expression, source_node = expression) {
1869
+ return set_loc(
1870
+ /** @type {AST.Expression} */ ({
1871
+ type: 'ConditionalExpression',
1872
+ test: {
1873
+ type: 'BinaryExpression',
1874
+ operator: '==',
1875
+ left: clone_expression_node(expression),
1876
+ right: {
1877
+ type: 'Literal',
1878
+ value: null,
1879
+ raw: 'null',
1880
+ metadata: { path: [] },
1881
+ },
1882
+ metadata: { path: [] },
1883
+ },
1884
+ consequent: {
1885
+ type: 'Literal',
1886
+ value: '',
1887
+ raw: "''",
1888
+ metadata: { path: [] },
1889
+ },
1890
+ alternate: {
1891
+ type: 'BinaryExpression',
1892
+ operator: '+',
1893
+ left: clone_expression_node(expression),
1894
+ right: {
1895
+ type: 'Literal',
1896
+ value: '',
1897
+ raw: "''",
1898
+ metadata: { path: [] },
1899
+ },
1900
+ metadata: { path: [] },
1901
+ },
1902
+ metadata: { path: [] },
1903
+ }),
1904
+ source_node,
1905
+ );
1906
+ }
1907
+
1829
1908
  /**
1830
1909
  * @param {any} attr
1831
1910
  * @returns {ESTreeJSX.JSXAttribute | ESTreeJSX.JSXSpreadAttribute}
package/types/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Program } from 'estree';
2
- import type { VolarMappingsResult } from '@tsrx/core/types';
2
+ import type { ParseOptions, VolarMappingsResult } from '@tsrx/core/types';
3
3
 
4
- export function parse(source: string, filename?: string): Program;
4
+ export function parse(source: string, filename?: string, options?: ParseOptions): Program;
5
5
 
6
6
  export function compile(
7
7
  source: string,
@@ -12,4 +12,8 @@ export function compile(
12
12
  css: { code: string; hash: string } | null;
13
13
  };
14
14
 
15
- export function compile_to_volar_mappings(source: string, filename?: string): VolarMappingsResult;
15
+ export function compile_to_volar_mappings(
16
+ source: string,
17
+ filename?: string,
18
+ options?: ParseOptions,
19
+ ): VolarMappingsResult;