swc-plugin-component-annotate 1.13.0 → 1.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swc-plugin-component-annotate",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Use SWC to automatically annotate React components with data attributes for component tracking",
5
5
  "author": "scttcper <scttcper@gmail.com>",
6
6
  "license": "MIT",
@@ -8,11 +8,6 @@
8
8
  "swc-plugin",
9
9
  "swc"
10
10
  ],
11
- "scripts": {
12
- "build": "cargo build --release --target wasm32-unknown-unknown",
13
- "test": "cargo test",
14
- "prepack": "cp -rf target/wasm32-unknown-unknown/release/swc_plugin_component_annotate.wasm ."
15
- },
16
11
  "main": "swc_plugin_component_annotate.wasm",
17
12
  "files": [
18
13
  "src",
@@ -28,7 +23,6 @@
28
23
  },
29
24
  "devDependencies": {},
30
25
  "peerDependencies": {},
31
- "packageManager": "pnpm@10.18.3",
32
26
  "release": {
33
27
  "branches": [
34
28
  "main"
@@ -36,5 +30,9 @@
36
30
  },
37
31
  "publishConfig": {
38
32
  "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "cargo build --release --target wasm32-unknown-unknown",
36
+ "test": "cargo test"
39
37
  }
40
- }
38
+ }
package/src/constants.rs CHANGED
@@ -1,125 +1,130 @@
1
1
  use rustc_hash::FxHashSet;
2
+ use std::sync::OnceLock;
2
3
 
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
- ];
4
+ pub fn default_ignored_elements() -> &'static FxHashSet<&'static str> {
5
+ static SET: OnceLock<FxHashSet<&'static str>> = OnceLock::new();
119
6
 
120
- for element in elements {
121
- set.insert(element);
122
- }
7
+ SET.get_or_init(|| {
8
+ let mut set = FxHashSet::default();
9
+ let elements = [
10
+ "a",
11
+ "abbr",
12
+ "address",
13
+ "area",
14
+ "article",
15
+ "aside",
16
+ "audio",
17
+ "b",
18
+ "base",
19
+ "bdi",
20
+ "bdo",
21
+ "blockquote",
22
+ "body",
23
+ "br",
24
+ "button",
25
+ "canvas",
26
+ "caption",
27
+ "cite",
28
+ "code",
29
+ "col",
30
+ "colgroup",
31
+ "data",
32
+ "datalist",
33
+ "dd",
34
+ "del",
35
+ "details",
36
+ "dfn",
37
+ "dialog",
38
+ "div",
39
+ "dl",
40
+ "dt",
41
+ "em",
42
+ "embed",
43
+ "fieldset",
44
+ "figure",
45
+ "footer",
46
+ "form",
47
+ "h1",
48
+ "h2",
49
+ "h3",
50
+ "h4",
51
+ "h5",
52
+ "h6",
53
+ "head",
54
+ "header",
55
+ "hgroup",
56
+ "hr",
57
+ "html",
58
+ "i",
59
+ "iframe",
60
+ "img",
61
+ "input",
62
+ "ins",
63
+ "kbd",
64
+ "keygen",
65
+ "label",
66
+ "legend",
67
+ "li",
68
+ "link",
69
+ "main",
70
+ "map",
71
+ "mark",
72
+ "menu",
73
+ "menuitem",
74
+ "meter",
75
+ "nav",
76
+ "noscript",
77
+ "object",
78
+ "ol",
79
+ "optgroup",
80
+ "option",
81
+ "output",
82
+ "p",
83
+ "param",
84
+ "pre",
85
+ "progress",
86
+ "q",
87
+ "rb",
88
+ "rp",
89
+ "rt",
90
+ "rtc",
91
+ "ruby",
92
+ "s",
93
+ "samp",
94
+ "script",
95
+ "section",
96
+ "select",
97
+ "small",
98
+ "source",
99
+ "span",
100
+ "strong",
101
+ "style",
102
+ "sub",
103
+ "summary",
104
+ "sup",
105
+ "table",
106
+ "tbody",
107
+ "td",
108
+ "template",
109
+ "textarea",
110
+ "tfoot",
111
+ "th",
112
+ "thead",
113
+ "time",
114
+ "title",
115
+ "tr",
116
+ "track",
117
+ "u",
118
+ "ul",
119
+ "var",
120
+ "video",
121
+ "wbr",
122
+ ];
123
123
 
124
- set
124
+ for element in elements {
125
+ set.insert(element);
126
+ }
127
+
128
+ set
129
+ })
125
130
  }
package/src/jsx_utils.rs CHANGED
@@ -6,15 +6,11 @@ use swc_core::ecma::ast::*;
6
6
  pub fn is_react_fragment(element: &JSXElementName) -> bool {
7
7
  match element {
8
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
- }
9
+ JSXElementName::JSXMemberExpr(member_expr) => matches!(
10
+ &member_expr.obj,
11
+ JSXObject::Ident(obj)
12
+ if obj.sym.as_ref() == "React" && member_expr.prop.sym.as_ref() == "Fragment"
13
+ ),
18
14
  JSXElementName::JSXNamespacedName(_) => false,
19
15
  #[cfg(swc_ast_unknown)]
20
16
  _ => panic!("unknown jsx element name"),
@@ -39,28 +35,42 @@ pub fn get_element_name(element: &JSXElementName) -> Cow<str> {
39
35
 
40
36
  /// Recursively build the name for member expressions (e.g., "Components.UI.Button")
41
37
  fn get_member_expression_name(member_expr: &JSXMemberExpr) -> String {
42
- let obj_name = match &member_expr.obj {
43
- JSXObject::Ident(ident) => ident.sym.as_ref(),
44
- JSXObject::JSXMemberExpr(nested_member) => {
45
- return format!(
46
- "{}.{}",
47
- get_member_expression_name(nested_member),
48
- member_expr.prop.sym
49
- );
38
+ fn member_expression_name_len(member_expr: &JSXMemberExpr) -> usize {
39
+ let obj_len = match &member_expr.obj {
40
+ JSXObject::Ident(ident) => ident.sym.len(),
41
+ JSXObject::JSXMemberExpr(nested_member) => member_expression_name_len(nested_member),
42
+ #[cfg(swc_ast_unknown)]
43
+ _ => panic!("unknown jsx object"),
44
+ };
45
+
46
+ obj_len + 1 + member_expr.prop.sym.len()
47
+ }
48
+
49
+ fn push_member_expression_name(target: &mut String, member_expr: &JSXMemberExpr) {
50
+ match &member_expr.obj {
51
+ JSXObject::Ident(ident) => target.push_str(ident.sym.as_ref()),
52
+ JSXObject::JSXMemberExpr(nested_member) => {
53
+ push_member_expression_name(target, nested_member);
54
+ }
55
+ #[cfg(swc_ast_unknown)]
56
+ _ => panic!("unknown jsx object"),
50
57
  }
51
- #[cfg(swc_ast_unknown)]
52
- _ => panic!("unknown jsx object"),
53
- };
54
58
 
55
- format!("{}.{}", obj_name, member_expr.prop.sym)
59
+ target.push('.');
60
+ target.push_str(member_expr.prop.sym.as_ref());
61
+ }
62
+
63
+ let mut output = String::with_capacity(member_expression_name_len(member_expr));
64
+ push_member_expression_name(&mut output, member_expr);
65
+ output
56
66
  }
57
67
 
58
68
  /// Check if a JSX element already has an attribute with the given name
59
69
  #[inline]
60
70
  pub fn has_attribute(element: &JSXOpeningElement, attr_name: &str) -> bool {
61
71
  element.attrs.iter().any(|attr| {
62
- matches!(attr, JSXAttrOrSpread::JSXAttr(jsx_attr)
63
- if matches!(&jsx_attr.name, JSXAttrName::Ident(ident)
72
+ matches!(attr, JSXAttrOrSpread::JSXAttr(jsx_attr)
73
+ if matches!(&jsx_attr.name, JSXAttrName::Ident(ident)
64
74
  if ident.sym.as_ref() == attr_name))
65
75
  })
66
76
  }
@@ -78,3 +88,25 @@ pub fn create_jsx_attr(name: &str, value: &str) -> JSXAttrOrSpread {
78
88
  })),
79
89
  })
80
90
  }
91
+
92
+ #[inline]
93
+ pub fn create_jsx_attr_with_ident(name: &IdentName, value: &str) -> JSXAttrOrSpread {
94
+ JSXAttrOrSpread::JSXAttr(JSXAttr {
95
+ span: Default::default(),
96
+ name: JSXAttrName::Ident(name.clone()),
97
+ value: Some(JSXAttrValue::Str(Str {
98
+ span: Default::default(),
99
+ value: value.into(),
100
+ raw: None,
101
+ })),
102
+ })
103
+ }
104
+
105
+ #[inline]
106
+ pub fn create_jsx_attr_with_ident_and_str(name: &IdentName, value: &Str) -> JSXAttrOrSpread {
107
+ JSXAttrOrSpread::JSXAttr(JSXAttr {
108
+ span: Default::default(),
109
+ name: JSXAttrName::Ident(name.clone()),
110
+ value: Some(JSXAttrValue::Str(value.clone())),
111
+ })
112
+ }
package/src/lib.rs CHANGED
@@ -8,7 +8,7 @@ use jsx_utils::*;
8
8
  use path_utils::{extract_absolute_path, extract_filename};
9
9
  use rustc_hash::FxHashSet;
10
10
  use swc_core::{
11
- common::FileName,
11
+ common::{FileName, DUMMY_SP},
12
12
  ecma::{
13
13
  ast::*,
14
14
  visit::{noop_visit_mut_type, VisitMut, VisitMutWith},
@@ -21,31 +21,55 @@ use swc_core::{
21
21
 
22
22
  pub struct ReactComponentAnnotateVisitor {
23
23
  config: PluginConfig,
24
- source_file_name: Option<String>,
25
- source_file_path: Option<String>,
24
+ source_file_name: Option<Str>,
25
+ source_file_path: Option<Str>,
26
26
  current_component_name: Option<String>,
27
- ignored_elements: FxHashSet<&'static str>,
27
+ ignored_elements: &'static FxHashSet<&'static str>,
28
28
  ignored_components_set: FxHashSet<String>,
29
+ component_attr_ident: IdentName,
30
+ element_attr_ident: IdentName,
31
+ source_file_attr_ident: IdentName,
32
+ source_path_attr_ident: Option<IdentName>,
29
33
  /// Track the local identifier name for `styled` from @emotion/styled
30
34
  styled_import: Option<String>,
31
35
  }
32
36
 
33
37
  impl ReactComponentAnnotateVisitor {
34
38
  pub fn new(config: PluginConfig, filename: &FileName) -> Self {
35
- let source_file_name = extract_filename(filename);
36
- let source_file_path = extract_absolute_path(filename);
39
+ let source_file_name = extract_filename(filename).map(|value| Str {
40
+ span: DUMMY_SP,
41
+ value: value.into(),
42
+ raw: None,
43
+ });
44
+ let source_file_path = extract_absolute_path(filename).map(|value| Str {
45
+ span: DUMMY_SP,
46
+ value: value.into(),
47
+ raw: None,
48
+ });
37
49
 
38
50
  // Pre-compute ignored components set for O(1) lookups
39
51
  let ignored_components_set: FxHashSet<String> =
40
52
  config.ignored_components.iter().cloned().collect();
53
+ let component_attr_ident = IdentName::new(config.component_attr_name().into(), DUMMY_SP);
54
+ let element_attr_ident = IdentName::new(config.element_attr_name().into(), DUMMY_SP);
55
+ let source_file_attr_ident =
56
+ IdentName::new(config.source_file_attr_name().into(), DUMMY_SP);
57
+ let source_path_attr_ident = config
58
+ .source_path_attr
59
+ .as_ref()
60
+ .map(|_| IdentName::new(config.source_path_attr_name().into(), DUMMY_SP));
41
61
 
42
62
  Self {
63
+ component_attr_ident,
43
64
  config,
65
+ element_attr_ident,
66
+ ignored_elements: constants::default_ignored_elements(),
67
+ ignored_components_set,
44
68
  source_file_name,
69
+ source_file_attr_ident,
45
70
  source_file_path,
71
+ source_path_attr_ident,
46
72
  current_component_name: None,
47
- ignored_elements: constants::default_ignored_elements(),
48
- ignored_components_set,
49
73
  styled_import: None,
50
74
  }
51
75
  }
@@ -113,11 +137,6 @@ impl ReactComponentAnnotateVisitor {
113
137
  fn add_attributes_to_element(&self, opening_element: &mut JSXOpeningElement) {
114
138
  let element_name = get_element_name(&opening_element.name);
115
139
 
116
- // Skip React fragments
117
- if is_react_fragment(&opening_element.name) {
118
- return;
119
- }
120
-
121
140
  // Check if component should be ignored
122
141
  if let Some(ref component_name) = self.current_component_name {
123
142
  if self.should_ignore_component(component_name) {
@@ -125,58 +144,71 @@ impl ReactComponentAnnotateVisitor {
125
144
  }
126
145
  }
127
146
 
128
- // Check if element should be ignored
129
147
  if self.should_ignore_component(&element_name) {
130
148
  return;
131
149
  }
132
150
 
133
151
  let is_ignored_html = self.should_ignore_element(&element_name);
134
-
135
- // Add element attribute (for non-HTML elements or when component name differs)
136
- if !is_ignored_html
152
+ let add_element_attr = !is_ignored_html
137
153
  && !has_attribute(opening_element, self.config.element_attr_name())
138
154
  && (self.config.component_attr_name() != self.config.element_attr_name()
139
- || self.current_component_name.is_none())
140
- {
141
- opening_element.attrs.push(create_jsx_attr(
142
- self.config.element_attr_name(),
155
+ || self.current_component_name.is_none());
156
+ let add_component_attr = self.current_component_name.is_some()
157
+ && !has_attribute(opening_element, self.config.component_attr_name());
158
+ let add_source_file_attr = self.source_file_name.is_some()
159
+ && (self.current_component_name.is_some() || !is_ignored_html)
160
+ && !has_attribute(opening_element, self.config.source_file_attr_name());
161
+ let add_source_path_attr = self.source_file_path.is_some()
162
+ && self.source_path_attr_ident.is_some()
163
+ && (self.current_component_name.is_some() || !is_ignored_html)
164
+ && !has_attribute(opening_element, self.config.source_path_attr_name());
165
+
166
+ let attr_count = usize::from(add_element_attr)
167
+ + usize::from(add_component_attr)
168
+ + usize::from(add_source_file_attr)
169
+ + usize::from(add_source_path_attr);
170
+
171
+ if attr_count > 0 {
172
+ opening_element.attrs.reserve(attr_count);
173
+ }
174
+
175
+ if add_element_attr {
176
+ opening_element.attrs.push(create_jsx_attr_with_ident(
177
+ &self.element_attr_ident,
143
178
  &element_name,
144
179
  ));
145
180
  }
146
181
 
147
- // Add component attribute (only for root elements)
148
- if let Some(ref component_name) = self.current_component_name {
149
- if !has_attribute(opening_element, self.config.component_attr_name()) {
150
- opening_element.attrs.push(create_jsx_attr(
151
- self.config.component_attr_name(),
182
+ if add_component_attr {
183
+ if let Some(ref component_name) = self.current_component_name {
184
+ opening_element.attrs.push(create_jsx_attr_with_ident(
185
+ &self.component_attr_ident,
152
186
  component_name,
153
187
  ));
154
188
  }
155
189
  }
156
190
 
157
- // Add source file attribute
158
- if let Some(ref source_file) = self.source_file_name {
159
- if (self.current_component_name.is_some() || !is_ignored_html)
160
- && !has_attribute(opening_element, self.config.source_file_attr_name())
161
- {
162
- opening_element.attrs.push(create_jsx_attr(
163
- self.config.source_file_attr_name(),
164
- source_file,
165
- ));
191
+ if add_source_file_attr {
192
+ if let Some(ref source_file) = self.source_file_name {
193
+ opening_element
194
+ .attrs
195
+ .push(create_jsx_attr_with_ident_and_str(
196
+ &self.source_file_attr_ident,
197
+ source_file,
198
+ ));
166
199
  }
167
200
  }
168
201
 
169
- // Add source path attribute (only if explicitly configured)
170
- if self.config.source_path_attr.is_some() {
171
- if let Some(ref source_path) = self.source_file_path {
172
- if (self.current_component_name.is_some() || !is_ignored_html)
173
- && !has_attribute(opening_element, self.config.source_path_attr_name())
174
- {
175
- opening_element.attrs.push(create_jsx_attr(
176
- self.config.source_path_attr_name(),
202
+ if add_source_path_attr {
203
+ if let (Some(ref source_path), Some(ref source_path_attr_ident)) =
204
+ (&self.source_file_path, &self.source_path_attr_ident)
205
+ {
206
+ opening_element
207
+ .attrs
208
+ .push(create_jsx_attr_with_ident_and_str(
209
+ source_path_attr_ident,
177
210
  source_path,
178
211
  ));
179
- }
180
212
  }
181
213
  }
182
214
  }
@@ -266,7 +298,12 @@ impl ReactComponentAnnotateVisitor {
266
298
  });
267
299
 
268
300
  // Build attributes in order: data attributes first, then spread
269
- let mut attrs = vec![];
301
+ let mut attrs = Vec::with_capacity(
302
+ 2 + usize::from(self.source_file_name.is_some())
303
+ + usize::from(
304
+ self.source_path_attr_ident.is_some() && self.source_file_path.is_some(),
305
+ ),
306
+ );
270
307
 
271
308
  // Add data-element attribute using the styled component variable name
272
309
  attrs.push(create_jsx_attr(
@@ -276,20 +313,20 @@ impl ReactComponentAnnotateVisitor {
276
313
 
277
314
  // Add data-source-file attribute
278
315
  if let Some(ref source_file) = self.source_file_name {
279
- attrs.push(create_jsx_attr(
280
- self.config.source_file_attr_name(),
316
+ attrs.push(create_jsx_attr_with_ident_and_str(
317
+ &self.source_file_attr_ident,
281
318
  source_file,
282
319
  ));
283
320
  }
284
321
 
285
322
  // Add data-source-path attribute (only if explicitly configured)
286
- if self.config.source_path_attr.is_some() {
287
- if let Some(ref source_path) = self.source_file_path {
288
- attrs.push(create_jsx_attr(
289
- self.config.source_path_attr_name(),
290
- source_path,
291
- ));
292
- }
323
+ if let (Some(ref source_path), Some(ref source_path_attr_ident)) =
324
+ (&self.source_file_path, &self.source_path_attr_ident)
325
+ {
326
+ attrs.push(create_jsx_attr_with_ident_and_str(
327
+ source_path_attr_ident,
328
+ source_path,
329
+ ));
293
330
  }
294
331
 
295
332
  // Add spread attribute AFTER data attributes: {...props}
File without changes