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 +22 -0
- package/README.md +188 -0
- package/package.json +29 -0
- package/src/config.rs +60 -0
- package/src/constants.rs +125 -0
- package/src/jsx_utils.rs +74 -0
- package/src/lib.rs +329 -0
- package/swc_plugin_component_annotate.wasm +0 -0
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
|
+
}
|
package/src/constants.rs
ADDED
@@ -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
|
+
}
|
package/src/jsx_utils.rs
ADDED
@@ -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
|
+
}
|
Binary file
|