swc-plugin-component-annotate 1.9.0 → 1.11.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # SWC Plugin: Component Annotate
1
+ # SWC Plugin: Component Annotate [![npm badge](https://img.shields.io/npm/v/swc-plugin-component-annotate)](https://www.npmjs.com/package/swc-plugin-component-annotate)
2
2
 
3
3
  A SWC plugin that automatically annotates React components with data attributes for component tracking and debugging.
4
4
 
@@ -24,8 +24,6 @@ This plugin transforms React components by adding data attributes that help with
24
24
 
25
25
  ```bash
26
26
  npm install --save-dev swc-plugin-component-annotate
27
- # or
28
- yarn add -D swc-plugin-component-annotate
29
27
  ```
30
28
 
31
29
  ## Usage
@@ -182,4 +180,4 @@ const MyComponent = () => {
182
180
 
183
181
  ## Related
184
182
 
185
- - [Sentry Babel Component Annotate Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/babel-plugin-component-annotate)
183
+ - [Sentry Babel Component Annotate Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/babel-plugin-component-annotate)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swc-plugin-component-annotate",
3
- "version": "1.9.0",
3
+ "version": "1.11.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",
@@ -19,11 +19,22 @@
19
19
  "swc_plugin_component_annotate.wasm"
20
20
  ],
21
21
  "homepage": "https://github.com/scttcper/swc-plugin-component-annotate#readme",
22
- "repository": "scttcper/swc-plugin-component-annotate",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/scttcper/swc-plugin-component-annotate.git"
25
+ },
23
26
  "bugs": {
24
27
  "url": "https://github.com/scttcper/swc-plugin-component-annotate/issues"
25
28
  },
26
29
  "devDependencies": {},
27
30
  "peerDependencies": {},
28
- "packageManager": "pnpm@10.15.0"
31
+ "packageManager": "pnpm@10.18.3",
32
+ "release": {
33
+ "branches": [
34
+ "main"
35
+ ]
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
29
40
  }
package/src/config.rs CHANGED
@@ -25,6 +25,10 @@ pub struct PluginConfig {
25
25
  /// Custom source path attribute name (overrides default and native setting)
26
26
  #[serde(default, rename = "source-path-attr")]
27
27
  pub source_path_attr: Option<String>,
28
+
29
+ /// Enable rewriting emotion styled components to inject data attributes
30
+ #[serde(default, rename = "rewrite-emotion-styled")]
31
+ pub experimental_rewrite_emotion_styled: bool,
28
32
  }
29
33
 
30
34
  impl PluginConfig {
package/src/lib.rs CHANGED
@@ -26,6 +26,8 @@ pub struct ReactComponentAnnotateVisitor {
26
26
  current_component_name: Option<String>,
27
27
  ignored_elements: FxHashSet<&'static str>,
28
28
  ignored_components_set: FxHashSet<String>,
29
+ /// Track the local identifier name for `styled` from @emotion/styled
30
+ styled_import: Option<String>,
29
31
  }
30
32
 
31
33
  impl ReactComponentAnnotateVisitor {
@@ -44,6 +46,7 @@ impl ReactComponentAnnotateVisitor {
44
46
  current_component_name: None,
45
47
  ignored_elements: constants::default_ignored_elements(),
46
48
  ignored_components_set,
49
+ styled_import: None,
47
50
  }
48
51
  }
49
52
 
@@ -210,11 +213,162 @@ impl ReactComponentAnnotateVisitor {
210
213
  _ => {}
211
214
  }
212
215
  }
216
+
217
+ /// Check if a call expression matches styled(ComponentRef) pattern
218
+ fn is_styled_call_with_component_ref(&self, call_expr: &CallExpr) -> Option<String> {
219
+ // Check if we have a tracked styled import
220
+ let styled_name = self.styled_import.as_ref()?;
221
+
222
+ // Check if the callee is the styled identifier
223
+ let callee_name = match call_expr.callee.as_expr() {
224
+ Some(expr) => match expr.as_ref() {
225
+ Expr::Ident(ident) => ident.sym.as_ref(),
226
+ _ => return None,
227
+ },
228
+ _ => return None,
229
+ };
230
+
231
+ if callee_name != styled_name {
232
+ return None;
233
+ }
234
+
235
+ // Check if the first argument is an identifier (component reference)
236
+ if let Some(ExprOrSpread { spread: None, expr }) = call_expr.args.first() {
237
+ if let Expr::Ident(ident) = expr.as_ref() {
238
+ return Some(ident.sym.to_string());
239
+ }
240
+ }
241
+
242
+ None
243
+ }
244
+
245
+ /// Transform styled(ComponentRef) to styled(props => <ComponentRef data-element="..." {...props} />)
246
+ fn transform_styled_call(
247
+ &self,
248
+ call_expr: &mut CallExpr,
249
+ ref_component_name: String,
250
+ styled_component_name: String,
251
+ ) {
252
+ use swc_core::common::{SyntaxContext, DUMMY_SP};
253
+
254
+ // Create the props parameter: props
255
+ let props_param = Pat::Ident(BindingIdent {
256
+ id: Ident::new("props".into(), DUMMY_SP, SyntaxContext::empty()),
257
+ type_ann: None,
258
+ });
259
+
260
+ // Build attributes in order: data attributes first, then spread
261
+ let mut attrs = vec![];
262
+
263
+ // Add data-element attribute using the styled component variable name
264
+ attrs.push(create_jsx_attr(
265
+ self.config.element_attr_name(),
266
+ &styled_component_name,
267
+ ));
268
+
269
+ // Add data-source-file attribute
270
+ if let Some(ref source_file) = self.source_file_name {
271
+ attrs.push(create_jsx_attr(
272
+ self.config.source_file_attr_name(),
273
+ source_file,
274
+ ));
275
+ }
276
+
277
+ // Add data-source-path attribute (only if explicitly configured)
278
+ if self.config.source_path_attr.is_some() {
279
+ if let Some(ref source_path) = self.source_file_path {
280
+ attrs.push(create_jsx_attr(
281
+ self.config.source_path_attr_name(),
282
+ source_path,
283
+ ));
284
+ }
285
+ }
286
+
287
+ // Add spread attribute AFTER data attributes: {...props}
288
+ attrs.push(JSXAttrOrSpread::SpreadElement(SpreadElement {
289
+ dot3_token: DUMMY_SP,
290
+ expr: Box::new(Expr::Ident(Ident::new(
291
+ "props".into(),
292
+ DUMMY_SP,
293
+ SyntaxContext::empty(),
294
+ ))),
295
+ }));
296
+
297
+ // Create JSX element: <ComponentRef data-element="..." data-source-file="..." {...props} />
298
+ let jsx_element = JSXElement {
299
+ span: DUMMY_SP,
300
+ opening: JSXOpeningElement {
301
+ name: JSXElementName::Ident(Ident::new(
302
+ ref_component_name.into(),
303
+ DUMMY_SP,
304
+ SyntaxContext::empty(),
305
+ )),
306
+ span: DUMMY_SP,
307
+ attrs,
308
+ self_closing: true,
309
+ type_args: None,
310
+ },
311
+ children: vec![],
312
+ closing: None,
313
+ };
314
+
315
+ // Create arrow function: props => <ComponentRef data-element="..." {...props} />
316
+ let arrow_func = ArrowExpr {
317
+ span: DUMMY_SP,
318
+ ctxt: SyntaxContext::empty(),
319
+ params: vec![props_param],
320
+ body: Box::new(BlockStmtOrExpr::Expr(Box::new(Expr::JSXElement(Box::new(
321
+ jsx_element,
322
+ ))))),
323
+ is_async: false,
324
+ is_generator: false,
325
+ type_params: None,
326
+ return_type: None,
327
+ };
328
+
329
+ // Replace the first argument with the arrow function
330
+ call_expr.args[0] = ExprOrSpread {
331
+ spread: None,
332
+ expr: Box::new(Expr::Arrow(arrow_func)),
333
+ };
334
+ }
213
335
  }
214
336
 
215
337
  impl VisitMut for ReactComponentAnnotateVisitor {
216
338
  noop_visit_mut_type!();
217
339
 
340
+ fn visit_mut_import_decl(&mut self, import_decl: &mut ImportDecl) {
341
+ // Track imports from @emotion/styled (only if enabled)
342
+ if self.config.experimental_rewrite_emotion_styled
343
+ && import_decl.src.value.as_ref() == "@emotion/styled"
344
+ {
345
+ for specifier in &import_decl.specifiers {
346
+ match specifier {
347
+ // Default import: import styled from '@emotion/styled'
348
+ ImportSpecifier::Default(default_import) => {
349
+ self.styled_import = Some(default_import.local.sym.to_string());
350
+ }
351
+ // Named import: import { styled } from '@emotion/styled'
352
+ ImportSpecifier::Named(named_import) => {
353
+ // Check if the imported name is 'default' or 'styled'
354
+ let imported_name = match &named_import.imported {
355
+ Some(ModuleExportName::Ident(ident)) => ident.sym.as_ref(),
356
+ None => named_import.local.sym.as_ref(),
357
+ _ => continue,
358
+ };
359
+
360
+ if imported_name == "default" || imported_name == "styled" {
361
+ self.styled_import = Some(named_import.local.sym.to_string());
362
+ }
363
+ }
364
+ _ => {}
365
+ }
366
+ }
367
+ }
368
+
369
+ import_decl.visit_mut_children_with(self);
370
+ }
371
+
218
372
  fn visit_mut_fn_decl(&mut self, func_decl: &mut FnDecl) {
219
373
  let component_name = func_decl.ident.sym.to_string();
220
374
  self.find_jsx_in_function_body(&mut func_decl.function, component_name);
@@ -228,6 +382,22 @@ impl VisitMut for ReactComponentAnnotateVisitor {
228
382
 
229
383
  if let Some(init) = &mut var_declarator.init {
230
384
  match init.as_mut() {
385
+ Expr::Call(call_expr) => {
386
+ // Check if this is a styled(ComponentRef) pattern (only if enabled)
387
+ if self.config.experimental_rewrite_emotion_styled {
388
+ if let Some(ref_component_name) =
389
+ self.is_styled_call_with_component_ref(call_expr)
390
+ {
391
+ // Transform styled(ComponentRef) to styled(props => <ComponentRef {...props} />)
392
+ // Use the styled component variable name (e.g., StyledButton) as data-element
393
+ self.transform_styled_call(
394
+ call_expr,
395
+ ref_component_name,
396
+ component_name.clone(),
397
+ );
398
+ }
399
+ }
400
+ }
231
401
  Expr::Arrow(arrow_func) => {
232
402
  self.current_component_name = Some(component_name);
233
403
 
Binary file