swc-plugin-component-annotate 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Engineering at FullStory
4
+ Copyright (c) 2024 Sentry
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # SWC Plugin: React Component Annotate
2
+
3
+ A SWC plugin that automatically annotates React components with data attributes for component tracking and debugging.
4
+
5
+ ## Overview
6
+
7
+ This plugin transforms React components by adding data attributes that help with tracking and debugging. It automatically identifies React components (function components, arrow function components, and class components) and adds the following attributes:
8
+
9
+ - `data-component`: The component name (added to root elements)
10
+ - `data-element`: The element/component name (added to non-HTML elements)
11
+ - `data-source-file`: The source filename
12
+
13
+ ## Features
14
+
15
+ - ✅ **Function Components**: `function MyComponent() { ... }`
16
+ - ✅ **Arrow Function Components**: `const MyComponent = () => { ... }`
17
+ - ✅ **Class Components**: `class MyComponent extends Component { ... }`
18
+ - ✅ **React Fragments**: Supports `Fragment`, `React.Fragment`, and `<>` syntax
19
+ - ✅ **Nested Components**: Properly handles component hierarchies
20
+ - ✅ **React Native Support**: Uses camelCase attributes when configured
21
+ - ✅ **Configurable**: Ignore specific components, annotate fragments, etc.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install --save-dev swc-plugin-react-component-annotate
27
+ # or
28
+ yarn add -D swc-plugin-react-component-annotate
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### Basic Configuration
34
+
35
+ Add the plugin to your `.swcrc` configuration:
36
+
37
+ ```json
38
+ {
39
+ "jsc": {
40
+ "experimental": {
41
+ "plugins": [
42
+ ["swc-plugin-react-component-annotate", {}]
43
+ ]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Configuration Options
50
+
51
+ ```json
52
+ {
53
+ "jsc": {
54
+ "experimental": {
55
+ "plugins": [
56
+ ["swc-plugin-react-component-annotate", {
57
+ "native": false,
58
+ "annotate-fragments": false,
59
+ "ignored-components": ["MyIgnoredComponent"],
60
+ "component-attr": "data-sentry-component",
61
+ "element-attr": "data-sentry-element",
62
+ "source-file-attr": "data-sentry-source-file"
63
+ }]
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ #### Options
71
+
72
+ - **`native`** (boolean, default: `false`): Use React Native attribute names (camelCase)
73
+ - `false`: `data-component`, `data-element`, `data-source-file`
74
+ - `true`: `dataComponent`, `dataElement`, `dataSourceFile`
75
+
76
+ - **`annotate-fragments`** (boolean, default: `false`): Whether to annotate fragment children with component information
77
+
78
+ - **`ignored-components`** (array, default: `[]`): List of component names to skip during annotation
79
+
80
+ - **`component-attr`** (string, optional): Custom component attribute name (overrides default and native setting)
81
+
82
+ - **`element-attr`** (string, optional): Custom element attribute name (overrides default and native setting)
83
+
84
+ - **`source-file-attr`** (string, optional): Custom source file attribute name (overrides default and native setting)
85
+
86
+ ### Sentry Integration
87
+
88
+ To use Sentry-specific attribute names for compatibility with Sentry's tracking:
89
+
90
+ ```json
91
+ {
92
+ "jsc": {
93
+ "experimental": {
94
+ "plugins": [
95
+ ["swc-plugin-react-component-annotate", {
96
+ "component-attr": "data-sentry-component",
97
+ "element-attr": "data-sentry-element",
98
+ "source-file-attr": "data-sentry-source-file"
99
+ }]
100
+ ]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ This will generate attributes like:
107
+ ```jsx
108
+ <div data-sentry-component="MyComponent" data-sentry-source-file="MyComponent.jsx">
109
+ <CustomButton data-sentry-element="CustomButton" data-sentry-source-file="MyComponent.jsx">
110
+ Click me
111
+ </CustomButton>
112
+ </div>
113
+ ```
114
+
115
+ ## Examples
116
+
117
+ ### Input
118
+
119
+ ```jsx
120
+ import React from 'react';
121
+
122
+ const MyComponent = () => {
123
+ return (
124
+ <div>
125
+ <h1>Hello World</h1>
126
+ <button>Click me</button>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ export default MyComponent;
132
+ ```
133
+
134
+ ### Output
135
+
136
+ ```jsx
137
+ import React from 'react';
138
+
139
+ const MyComponent = () => {
140
+ return (
141
+ <div data-component="MyComponent" data-source-file="MyComponent.jsx">
142
+ <h1>Hello World</h1>
143
+ <button>Click me</button>
144
+ </div>
145
+ );
146
+ };
147
+
148
+ export default MyComponent;
149
+ ```
150
+
151
+ ### Class Component Example
152
+
153
+ ```jsx
154
+ // Input
155
+ class MyClassComponent extends Component {
156
+ render() {
157
+ return <div><h1>Hello from class</h1></div>;
158
+ }
159
+ }
160
+
161
+ // Output
162
+ class MyClassComponent extends Component {
163
+ render() {
164
+ return <div data-component="MyClassComponent" data-source-file="MyComponent.jsx">
165
+ <h1>Hello from class</h1>
166
+ </div>;
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### React Native Example
172
+
173
+ With `"native": true`:
174
+
175
+ ```jsx
176
+ // Output
177
+ const MyComponent = () => {
178
+ return (
179
+ <View dataComponent="MyComponent" dataSourceFile="MyComponent.jsx">
180
+ <Text>Hello World</Text>
181
+ </View>
182
+ );
183
+ };
184
+ ```
185
+
186
+ ## Related
187
+
188
+ - [Sentry Babel Component Annotate Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/babel-plugin-component-annotate)
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "swc-plugin-component-annotate",
3
+ "version": "1.0.0",
4
+ "description": "Use SWC to automatically annotate React components with data attributes for component tracking",
5
+ "author": "scttcper <scttcper@gmail.com>",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "swc-plugin",
9
+ "swc"
10
+ ],
11
+ "scripts": {
12
+ "build": "cargo build --target wasm32-unknown-unknown",
13
+ "test": "cargo test",
14
+ "prepack": "cp -rf target/wasm32-unknown-unknown/release/swc_plugin_component_annotate.wasm ."
15
+ },
16
+ "main": "swc_plugin_component_annotate.wasm",
17
+ "files": [
18
+ "src",
19
+ "swc_plugin_component_annotate.wasm"
20
+ ],
21
+ "homepage": "https://github.com/scttcper/swc-plugin-component-annotate#readme",
22
+ "repository": "scttcper/swc-plugin-component-annotate",
23
+ "bugs": {
24
+ "url": "https://github.com/scttcper/swc-plugin-component-annotate/issues"
25
+ },
26
+ "devDependencies": {},
27
+ "peerDependencies": {},
28
+ "packageManager": "pnpm@10.11.0"
29
+ }
package/src/config.rs ADDED
@@ -0,0 +1,60 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ #[derive(Debug, Clone, Serialize, Deserialize, Default)]
4
+ pub struct PluginConfig {
5
+ /// Use React Native attribute names (camelCase) instead of web attributes (kebab-case)
6
+ #[serde(default)]
7
+ pub native: bool,
8
+
9
+ /// Whether to annotate fragment children with component information
10
+ #[serde(default, rename = "annotate-fragments")]
11
+ pub annotate_fragments: bool,
12
+
13
+ /// List of component names to ignore during annotation
14
+ #[serde(default, rename = "ignored-components")]
15
+ pub ignored_components: Vec<String>,
16
+
17
+ /// Custom component attribute name (overrides default and native setting)
18
+ #[serde(default, rename = "component-attr")]
19
+ pub component_attr: Option<String>,
20
+
21
+ /// Custom element attribute name (overrides default and native setting)
22
+ #[serde(default, rename = "element-attr")]
23
+ pub element_attr: Option<String>,
24
+
25
+ /// Custom source file attribute name (overrides default and native setting)
26
+ #[serde(default, rename = "source-file-attr")]
27
+ pub source_file_attr: Option<String>,
28
+ }
29
+
30
+ impl PluginConfig {
31
+ pub fn component_attr_name(&self) -> &str {
32
+ if let Some(ref custom) = self.component_attr {
33
+ custom
34
+ } else if self.native {
35
+ "dataComponent"
36
+ } else {
37
+ "data-component"
38
+ }
39
+ }
40
+
41
+ pub fn element_attr_name(&self) -> &str {
42
+ if let Some(ref custom) = self.element_attr {
43
+ custom
44
+ } else if self.native {
45
+ "dataElement"
46
+ } else {
47
+ "data-element"
48
+ }
49
+ }
50
+
51
+ pub fn source_file_attr_name(&self) -> &str {
52
+ if let Some(ref custom) = self.source_file_attr {
53
+ custom
54
+ } else if self.native {
55
+ "dataSourceFile"
56
+ } else {
57
+ "data-source-file"
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,125 @@
1
+ use rustc_hash::FxHashSet;
2
+
3
+ pub fn default_ignored_elements() -> FxHashSet<&'static str> {
4
+ let mut set = FxHashSet::default();
5
+ let elements = [
6
+ "a",
7
+ "abbr",
8
+ "address",
9
+ "area",
10
+ "article",
11
+ "aside",
12
+ "audio",
13
+ "b",
14
+ "base",
15
+ "bdi",
16
+ "bdo",
17
+ "blockquote",
18
+ "body",
19
+ "br",
20
+ "button",
21
+ "canvas",
22
+ "caption",
23
+ "cite",
24
+ "code",
25
+ "col",
26
+ "colgroup",
27
+ "data",
28
+ "datalist",
29
+ "dd",
30
+ "del",
31
+ "details",
32
+ "dfn",
33
+ "dialog",
34
+ "div",
35
+ "dl",
36
+ "dt",
37
+ "em",
38
+ "embed",
39
+ "fieldset",
40
+ "figure",
41
+ "footer",
42
+ "form",
43
+ "h1",
44
+ "h2",
45
+ "h3",
46
+ "h4",
47
+ "h5",
48
+ "h6",
49
+ "head",
50
+ "header",
51
+ "hgroup",
52
+ "hr",
53
+ "html",
54
+ "i",
55
+ "iframe",
56
+ "img",
57
+ "input",
58
+ "ins",
59
+ "kbd",
60
+ "keygen",
61
+ "label",
62
+ "legend",
63
+ "li",
64
+ "link",
65
+ "main",
66
+ "map",
67
+ "mark",
68
+ "menu",
69
+ "menuitem",
70
+ "meter",
71
+ "nav",
72
+ "noscript",
73
+ "object",
74
+ "ol",
75
+ "optgroup",
76
+ "option",
77
+ "output",
78
+ "p",
79
+ "param",
80
+ "pre",
81
+ "progress",
82
+ "q",
83
+ "rb",
84
+ "rp",
85
+ "rt",
86
+ "rtc",
87
+ "ruby",
88
+ "s",
89
+ "samp",
90
+ "script",
91
+ "section",
92
+ "select",
93
+ "small",
94
+ "source",
95
+ "span",
96
+ "strong",
97
+ "style",
98
+ "sub",
99
+ "summary",
100
+ "sup",
101
+ "table",
102
+ "tbody",
103
+ "td",
104
+ "template",
105
+ "textarea",
106
+ "tfoot",
107
+ "th",
108
+ "thead",
109
+ "time",
110
+ "title",
111
+ "tr",
112
+ "track",
113
+ "u",
114
+ "ul",
115
+ "var",
116
+ "video",
117
+ "wbr",
118
+ ];
119
+
120
+ for element in elements {
121
+ set.insert(element);
122
+ }
123
+
124
+ set
125
+ }
@@ -0,0 +1,74 @@
1
+ use std::borrow::Cow;
2
+ use swc_core::ecma::ast::*;
3
+
4
+ /// Check if a JSX element is a React Fragment
5
+ #[inline]
6
+ pub fn is_react_fragment(element: &JSXElementName) -> bool {
7
+ match element {
8
+ JSXElementName::Ident(ident) => ident.sym.as_ref() == "Fragment",
9
+ JSXElementName::JSXMemberExpr(member_expr) => {
10
+ // Check for React.Fragment
11
+ if let JSXObject::Ident(obj) = &member_expr.obj {
12
+ if obj.sym.as_ref() == "React" {
13
+ return member_expr.prop.sym.as_ref() == "Fragment";
14
+ }
15
+ }
16
+ false
17
+ }
18
+ _ => false,
19
+ }
20
+ }
21
+
22
+ /// Extract the element name from a JSX element
23
+ #[inline]
24
+ pub fn get_element_name(element: &JSXElementName) -> Cow<str> {
25
+ match element {
26
+ JSXElementName::Ident(ident) => Cow::Borrowed(ident.sym.as_ref()),
27
+ JSXElementName::JSXMemberExpr(member_expr) => {
28
+ Cow::Owned(get_member_expression_name(member_expr))
29
+ }
30
+ JSXElementName::JSXNamespacedName(namespaced) => {
31
+ Cow::Owned(format!("{}:{}", namespaced.ns.sym, namespaced.name.sym))
32
+ }
33
+ }
34
+ }
35
+
36
+ /// Recursively build the name for member expressions (e.g., "Components.UI.Button")
37
+ fn get_member_expression_name(member_expr: &JSXMemberExpr) -> String {
38
+ let obj_name = match &member_expr.obj {
39
+ JSXObject::Ident(ident) => ident.sym.as_ref(),
40
+ JSXObject::JSXMemberExpr(nested_member) => {
41
+ return format!(
42
+ "{}.{}",
43
+ get_member_expression_name(nested_member),
44
+ member_expr.prop.sym
45
+ );
46
+ }
47
+ };
48
+
49
+ format!("{}.{}", obj_name, member_expr.prop.sym)
50
+ }
51
+
52
+ /// Check if a JSX element already has an attribute with the given name
53
+ #[inline]
54
+ pub fn has_attribute(element: &JSXOpeningElement, attr_name: &str) -> bool {
55
+ element.attrs.iter().any(|attr| {
56
+ matches!(attr, JSXAttrOrSpread::JSXAttr(jsx_attr)
57
+ if matches!(&jsx_attr.name, JSXAttrName::Ident(ident)
58
+ if ident.sym.as_ref() == attr_name))
59
+ })
60
+ }
61
+
62
+ /// Create a JSX attribute with a string value
63
+ #[inline]
64
+ pub fn create_jsx_attr(name: &str, value: &str) -> JSXAttrOrSpread {
65
+ JSXAttrOrSpread::JSXAttr(JSXAttr {
66
+ span: Default::default(),
67
+ name: JSXAttrName::Ident(IdentName::new(name.into(), Default::default())),
68
+ value: Some(JSXAttrValue::Lit(Lit::Str(Str {
69
+ span: Default::default(),
70
+ value: value.into(),
71
+ raw: None,
72
+ }))),
73
+ })
74
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,329 @@
1
+ pub mod config;
2
+ mod constants;
3
+ mod jsx_utils;
4
+
5
+ use config::PluginConfig;
6
+ use jsx_utils::*;
7
+ use rustc_hash::FxHashSet;
8
+ use swc_core::{
9
+ common::FileName,
10
+ ecma::{
11
+ ast::*,
12
+ visit::{noop_visit_mut_type, VisitMut, VisitMutWith},
13
+ },
14
+ plugin::{plugin_transform, proxies::TransformPluginProgramMetadata},
15
+ };
16
+
17
+ pub struct ReactComponentAnnotateVisitor {
18
+ config: PluginConfig,
19
+ source_file_name: Option<String>,
20
+ current_component_name: Option<String>,
21
+ ignored_elements: FxHashSet<&'static str>,
22
+ ignored_components_set: FxHashSet<String>,
23
+ }
24
+
25
+ impl ReactComponentAnnotateVisitor {
26
+ pub fn new(config: PluginConfig, filename: &FileName) -> Self {
27
+ let source_file_name = extract_filename(filename);
28
+
29
+ // Pre-compute ignored components set for O(1) lookups
30
+ let ignored_components_set: FxHashSet<String> =
31
+ config.ignored_components.iter().cloned().collect();
32
+
33
+ Self {
34
+ config,
35
+ source_file_name,
36
+ current_component_name: None,
37
+ ignored_elements: constants::default_ignored_elements(),
38
+ ignored_components_set,
39
+ }
40
+ }
41
+
42
+ #[inline]
43
+ fn should_ignore_component(&self, component_name: &str) -> bool {
44
+ self.ignored_components_set.contains(component_name)
45
+ }
46
+
47
+ #[inline]
48
+ fn should_ignore_element(&self, element_name: &str) -> bool {
49
+ self.ignored_elements.contains(element_name)
50
+ }
51
+
52
+ fn process_jsx_element(&mut self, element: &mut JSXElement) {
53
+ self.add_attributes_to_element(&mut element.opening);
54
+
55
+ // Process children
56
+ for child in &mut element.children {
57
+ match child {
58
+ JSXElementChild::JSXElement(jsx_element) => {
59
+ // Children don't get component name, only element name
60
+ let prev_component = self.current_component_name.take();
61
+ jsx_element.visit_mut_with(self);
62
+ self.current_component_name = prev_component;
63
+ }
64
+ JSXElementChild::JSXFragment(jsx_fragment) => {
65
+ let prev_component = if self.config.annotate_fragments {
66
+ // Keep component name for first child of fragment
67
+ self.current_component_name.clone()
68
+ } else {
69
+ self.current_component_name.take()
70
+ };
71
+ jsx_fragment.visit_mut_with(self);
72
+ self.current_component_name = prev_component;
73
+ }
74
+ _ => {}
75
+ }
76
+ }
77
+ }
78
+
79
+ fn process_jsx_fragment(&mut self, fragment: &mut JSXFragment) {
80
+ // Process children
81
+ let mut first_element_processed = false;
82
+ for child in &mut fragment.children {
83
+ match child {
84
+ JSXElementChild::JSXElement(jsx_element) => {
85
+ if self.config.annotate_fragments && !first_element_processed {
86
+ // First child of fragment gets component name
87
+ first_element_processed = true;
88
+ jsx_element.visit_mut_with(self);
89
+ } else {
90
+ // Other children don't get component name
91
+ let prev_component = self.current_component_name.take();
92
+ jsx_element.visit_mut_with(self);
93
+ self.current_component_name = prev_component;
94
+ }
95
+ }
96
+ JSXElementChild::JSXFragment(jsx_fragment) => {
97
+ let prev_component =
98
+ if self.config.annotate_fragments && !first_element_processed {
99
+ first_element_processed = true;
100
+ self.current_component_name.clone()
101
+ } else {
102
+ self.current_component_name.take()
103
+ };
104
+ jsx_fragment.visit_mut_with(self);
105
+ self.current_component_name = prev_component;
106
+ }
107
+ _ => {}
108
+ }
109
+ }
110
+ }
111
+
112
+ fn add_attributes_to_element(&self, opening_element: &mut JSXOpeningElement) {
113
+ let element_name = get_element_name(&opening_element.name);
114
+
115
+ // Skip React fragments
116
+ if is_react_fragment(&opening_element.name) {
117
+ return;
118
+ }
119
+
120
+ // Check if component should be ignored
121
+ if let Some(ref component_name) = self.current_component_name {
122
+ if self.should_ignore_component(component_name) {
123
+ return;
124
+ }
125
+ }
126
+
127
+ // Check if element should be ignored
128
+ if self.should_ignore_component(&element_name) {
129
+ return;
130
+ }
131
+
132
+ let is_ignored_html = self.should_ignore_element(&element_name);
133
+
134
+ // Add element attribute (for non-HTML elements or when component name differs)
135
+ if !is_ignored_html
136
+ && !has_attribute(opening_element, self.config.element_attr_name())
137
+ && (self.config.component_attr_name() != self.config.element_attr_name()
138
+ || self.current_component_name.is_none())
139
+ {
140
+ opening_element.attrs.push(create_jsx_attr(
141
+ self.config.element_attr_name(),
142
+ &element_name,
143
+ ));
144
+ }
145
+
146
+ // Add component attribute (only for root elements)
147
+ if let Some(ref component_name) = self.current_component_name {
148
+ if !has_attribute(opening_element, self.config.component_attr_name()) {
149
+ opening_element.attrs.push(create_jsx_attr(
150
+ self.config.component_attr_name(),
151
+ component_name,
152
+ ));
153
+ }
154
+ }
155
+
156
+ // Add source file attribute
157
+ if let Some(ref source_file) = self.source_file_name {
158
+ if (self.current_component_name.is_some() || !is_ignored_html)
159
+ && !has_attribute(opening_element, self.config.source_file_attr_name())
160
+ {
161
+ opening_element.attrs.push(create_jsx_attr(
162
+ self.config.source_file_attr_name(),
163
+ source_file,
164
+ ));
165
+ }
166
+ }
167
+ }
168
+
169
+ fn find_jsx_in_function_body(&mut self, func: &mut Function, component_name: String) {
170
+ if let Some(body) = &mut func.body {
171
+ self.current_component_name = Some(component_name);
172
+
173
+ // Look for return statements
174
+ for stmt in &mut body.stmts {
175
+ if let Stmt::Return(return_stmt) = stmt {
176
+ if let Some(arg) = &mut return_stmt.arg {
177
+ self.process_return_expression(arg);
178
+ }
179
+ }
180
+ }
181
+
182
+ self.current_component_name = None;
183
+ }
184
+ }
185
+
186
+ fn process_return_expression(&mut self, expr: &mut Expr) {
187
+ match expr {
188
+ Expr::JSXElement(jsx_element) => {
189
+ jsx_element.visit_mut_with(self);
190
+ }
191
+ Expr::JSXFragment(jsx_fragment) => {
192
+ jsx_fragment.visit_mut_with(self);
193
+ }
194
+ Expr::Cond(cond_expr) => {
195
+ // Handle ternary expressions
196
+ self.process_return_expression(&mut cond_expr.cons);
197
+ self.process_return_expression(&mut cond_expr.alt);
198
+ }
199
+ Expr::Paren(paren_expr) => {
200
+ self.process_return_expression(&mut paren_expr.expr);
201
+ }
202
+ _ => {}
203
+ }
204
+ }
205
+ }
206
+
207
+ impl VisitMut for ReactComponentAnnotateVisitor {
208
+ noop_visit_mut_type!();
209
+
210
+ fn visit_mut_fn_decl(&mut self, func_decl: &mut FnDecl) {
211
+ let component_name = func_decl.ident.sym.to_string();
212
+ self.find_jsx_in_function_body(&mut func_decl.function, component_name);
213
+ func_decl.visit_mut_children_with(self);
214
+ }
215
+
216
+ fn visit_mut_var_declarator(&mut self, var_declarator: &mut VarDeclarator) {
217
+ // Handle arrow functions and function expressions assigned to variables
218
+ if let Pat::Ident(ident) = &var_declarator.name {
219
+ let component_name = ident.id.sym.to_string();
220
+
221
+ if let Some(init) = &mut var_declarator.init {
222
+ match init.as_mut() {
223
+ Expr::Arrow(arrow_func) => {
224
+ self.current_component_name = Some(component_name);
225
+
226
+ match arrow_func.body.as_mut() {
227
+ BlockStmtOrExpr::BlockStmt(block) => {
228
+ // Look for return statements in block
229
+ for stmt in &mut block.stmts {
230
+ if let Stmt::Return(return_stmt) = stmt {
231
+ if let Some(arg) = &mut return_stmt.arg {
232
+ self.process_return_expression(arg);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ BlockStmtOrExpr::Expr(expr) => {
238
+ // Direct expression return
239
+ self.process_return_expression(expr);
240
+ }
241
+ }
242
+
243
+ self.current_component_name = None;
244
+ }
245
+ Expr::Fn(func_expr) => {
246
+ self.find_jsx_in_function_body(&mut func_expr.function, component_name);
247
+ }
248
+ _ => {}
249
+ }
250
+ }
251
+ }
252
+
253
+ var_declarator.visit_mut_children_with(self);
254
+ }
255
+
256
+ fn visit_mut_class_decl(&mut self, class_decl: &mut ClassDecl) {
257
+ let component_name = class_decl.ident.sym.to_string();
258
+
259
+ // Look for render method
260
+ for member in &mut class_decl.class.body {
261
+ if let ClassMember::Method(method) = member {
262
+ if let PropName::Ident(ident) = &method.key {
263
+ if ident.sym.as_ref() == "render" {
264
+ if let Some(body) = &mut method.function.body {
265
+ self.current_component_name = Some(component_name.clone());
266
+
267
+ // Look for return statements
268
+ for stmt in &mut body.stmts {
269
+ if let Stmt::Return(return_stmt) = stmt {
270
+ if let Some(arg) = &mut return_stmt.arg {
271
+ self.process_return_expression(arg);
272
+ }
273
+ }
274
+ }
275
+
276
+ self.current_component_name = None;
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ class_decl.visit_mut_children_with(self);
284
+ }
285
+
286
+ fn visit_mut_jsx_element(&mut self, jsx_element: &mut JSXElement) {
287
+ self.process_jsx_element(jsx_element);
288
+ }
289
+
290
+ fn visit_mut_jsx_fragment(&mut self, jsx_fragment: &mut JSXFragment) {
291
+ self.process_jsx_fragment(jsx_fragment);
292
+ }
293
+ }
294
+
295
+ fn extract_filename(filename: &FileName) -> Option<String> {
296
+ match filename {
297
+ FileName::Real(path) => path
298
+ .file_name()
299
+ .and_then(|name| name.to_str())
300
+ .map(|s| s.to_string()),
301
+ FileName::Custom(custom) => {
302
+ if custom.contains('/') {
303
+ custom.split('/').last().map(|s| s.to_string())
304
+ } else if custom.contains('\\') {
305
+ custom.split('\\').last().map(|s| s.to_string())
306
+ } else {
307
+ Some(custom.clone())
308
+ }
309
+ }
310
+ _ => None,
311
+ }
312
+ }
313
+
314
+ #[plugin_transform]
315
+ pub fn process_transform(
316
+ mut program: Program,
317
+ metadata: TransformPluginProgramMetadata,
318
+ ) -> Program {
319
+ let config = if let Some(config_str) = metadata.get_transform_plugin_config() {
320
+ serde_json::from_str::<PluginConfig>(&config_str).unwrap_or_default()
321
+ } else {
322
+ PluginConfig::default()
323
+ };
324
+
325
+ let mut visitor =
326
+ ReactComponentAnnotateVisitor::new(config, &FileName::Custom("unknown".to_string()));
327
+ program.visit_mut_with(&mut visitor);
328
+ program
329
+ }