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 +2 -4
- package/package.json +14 -3
- package/src/config.rs +4 -0
- package/src/lib.rs +170 -0
- package/swc_plugin_component_annotate.wasm +0 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SWC Plugin: Component Annotate
|
|
1
|
+
# 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.
|
|
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":
|
|
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.
|
|
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
|