diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 8ee09f5ed22cc..f0405a9129d55 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -37,6 +37,7 @@ mod regexp; mod typescript; mod decorator; +mod plugins; use common::Common; use context::{TransformCtx, TraverseCtx}; @@ -56,6 +57,7 @@ use rustc_hash::FxHashMap; use state::TransformState; use typescript::TypeScript; +use crate::plugins::Plugins; pub use crate::{ common::helper_loader::{Helper, HelperLoaderMode, HelperLoaderOptions}, compiler_assumptions::CompilerAssumptions, @@ -73,6 +75,7 @@ pub use crate::{ ESTarget, Engine, EngineTargets, EnvOptions, Module, TransformOptions, babel::{BabelEnvOptions, BabelOptions}, }, + plugins::{PluginsOptions, StyledComponentsOptions}, proposals::ProposalOptions, typescript::{RewriteExtensionsMode, TypeScriptOptions}, }; @@ -92,6 +95,7 @@ pub struct Transformer<'a> { typescript: TypeScriptOptions, decorator: DecoratorOptions, + plugins: PluginsOptions, jsx: JsxOptions, env: EnvOptions, proposals: ProposalOptions, @@ -105,6 +109,7 @@ impl<'a> Transformer<'a> { allocator, typescript: options.typescript.clone(), decorator: options.decorator, + plugins: options.plugins.clone(), jsx: options.jsx.clone(), env: options.env, proposals: options.proposals, @@ -134,6 +139,7 @@ impl<'a> Transformer<'a> { let mut transformer = TransformerImpl { common: Common::new(&self.env, &self.ctx), decorator: Decorator::new(self.decorator, &self.ctx), + plugins: Plugins::new(self.plugins, &self.ctx), explicit_resource_management: self .proposals .explicit_resource_management @@ -171,6 +177,7 @@ struct TransformerImpl<'a, 'ctx> { // NOTE: all callbacks must run in order. x0_typescript: Option>, decorator: Decorator<'a, 'ctx>, + plugins: Plugins<'a, 'ctx>, explicit_resource_management: Option>, x1_jsx: Jsx<'a, 'ctx>, x2_es2022: ES2022<'a, 'ctx>, @@ -191,6 +198,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_program(program, ctx); } + self.plugins.enter_program(program, ctx); self.x1_jsx.enter_program(program, ctx); if let Some(explicit_resource_management) = self.explicit_resource_management.as_mut() { explicit_resource_management.enter_program(program, ctx); @@ -236,6 +244,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_variable_declarator(decl, ctx); } + self.plugins.enter_variable_declarator(decl, ctx); } fn enter_big_int_literal(&mut self, node: &mut BigIntLiteral<'a>, ctx: &mut TraverseCtx<'a>) { @@ -268,6 +277,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_call_expression(expr, ctx); } + self.plugins.enter_call_expression(expr, ctx); self.x1_jsx.enter_call_expression(expr, ctx); } @@ -318,6 +328,7 @@ impl<'a> Traverse<'a, TransformState<'a>> for TransformerImpl<'a, '_> { if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_expression(expr, ctx); } + self.plugins.enter_expression(expr, ctx); self.x2_es2022.enter_expression(expr, ctx); self.x2_es2021.enter_expression(expr, ctx); self.x2_es2020.enter_expression(expr, ctx); diff --git a/crates/oxc_transformer/src/options/babel/plugins.rs b/crates/oxc_transformer/src/options/babel/plugins.rs index dd35c00c86101..0cd0f5b255d65 100644 --- a/crates/oxc_transformer/src/options/babel/plugins.rs +++ b/crates/oxc_transformer/src/options/babel/plugins.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use crate::{ DecoratorOptions, TypeScriptOptions, es2015::ArrowFunctionsOptions, es2018::ObjectRestSpreadOptions, es2022::ClassPropertiesOptions, jsx::JsxOptions, + plugins::StyledComponentsOptions, }; use super::PluginPresetEntries; @@ -73,6 +74,8 @@ pub struct BabelPlugins { pub legacy_decorator: Option, // Proposals pub explicit_resource_management: bool, + // Built-in plugins + pub styled_components: Option, } impl TryFrom for BabelPlugins { @@ -161,6 +164,12 @@ impl TryFrom for BabelPlugins { entry.value::().map_err(|err| p.errors.push(err)).ok(); } "proposal-explicit-resource-management" => p.explicit_resource_management = true, + "styled-components" => { + p.styled_components = entry + .value::() + .map_err(|err| p.errors.push(err)) + .ok(); + } s => p.unsupported.push(s.to_string()), } } diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index f0c48c53a9def..36e4e0856624a 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -14,6 +14,7 @@ use crate::{ es2021::ES2021Options, es2022::ES2022Options, jsx::JsxOptions, + plugins::{PluginsOptions, StyledComponentsOptions}, proposals::ProposalOptions, regexp::RegExpOptions, typescript::TypeScriptOptions, @@ -69,6 +70,9 @@ pub struct TransformOptions { /// Proposals pub proposals: ProposalOptions, + /// Plugins + pub plugins: PluginsOptions, + pub helper_loader: HelperLoaderOptions, } @@ -90,6 +94,7 @@ impl TransformOptions { }, env: EnvOptions::enable_all(/* include_unfinished_plugins */ false), proposals: ProposalOptions::default(), + plugins: PluginsOptions { styled_components: Some(StyledComponentsOptions::default()) }, helper_loader: HelperLoaderOptions { mode: HelperLoaderMode::Runtime, ..Default::default() @@ -258,6 +263,11 @@ impl TryFrom<&BabelOptions> for TransformOptions { ..HelperLoaderOptions::default() }; + let mut plugins = PluginsOptions::default(); + if let Some(styled_components) = &options.plugins.styled_components { + plugins.styled_components = Some(styled_components.clone()); + } + Ok(Self { cwd: options.cwd.clone().unwrap_or_default(), assumptions: options.assumptions, @@ -280,6 +290,7 @@ impl TryFrom<&BabelOptions> for TransformOptions { explicit_resource_management: options.plugins.explicit_resource_management, }, helper_loader, + plugins, }) } } diff --git a/crates/oxc_transformer/src/plugins/mod.rs b/crates/oxc_transformer/src/plugins/mod.rs new file mode 100644 index 0000000000000..a7a491cf31eb7 --- /dev/null +++ b/crates/oxc_transformer/src/plugins/mod.rs @@ -0,0 +1,62 @@ +mod options; +mod styled_components; + +pub use options::PluginsOptions; +use oxc_ast::ast::*; +use oxc_traverse::Traverse; +pub use styled_components::StyledComponentsOptions; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + plugins::styled_components::StyledComponents, + state::TransformState, +}; + +pub struct Plugins<'a, 'ctx> { + styled_components: StyledComponents<'a, 'ctx>, + options: PluginsOptions, +} + +impl<'a, 'ctx> Plugins<'a, 'ctx> { + pub fn new(options: PluginsOptions, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { + styled_components: StyledComponents::new( + options.styled_components.clone().unwrap_or_default(), + ctx, + ), + options, + } + } +} + +impl<'a> Traverse<'a, TransformState<'a>> for Plugins<'a, '_> { + fn enter_program(&mut self, node: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.styled_components.is_some() { + self.styled_components.enter_program(node, ctx); + } + } + + fn enter_variable_declarator( + &mut self, + node: &mut VariableDeclarator<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.styled_components.is_some() { + self.styled_components.enter_variable_declarator(node, ctx); + } + } + + fn enter_expression( + &mut self, + node: &mut Expression<'a>, + ctx: &mut oxc_traverse::TraverseCtx<'a, TransformState<'a>>, + ) { + self.styled_components.enter_expression(node, ctx); + } + + fn enter_call_expression(&mut self, node: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.styled_components.is_some() { + self.styled_components.enter_call_expression(node, ctx); + } + } +} diff --git a/crates/oxc_transformer/src/plugins/options.rs b/crates/oxc_transformer/src/plugins/options.rs new file mode 100644 index 0000000000000..160eaaa1ad1ab --- /dev/null +++ b/crates/oxc_transformer/src/plugins/options.rs @@ -0,0 +1,6 @@ +use super::StyledComponentsOptions; + +#[derive(Default, Debug, Clone)] +pub struct PluginsOptions { + pub styled_components: Option, +} diff --git a/crates/oxc_transformer/src/plugins/styled_components.rs b/crates/oxc_transformer/src/plugins/styled_components.rs new file mode 100644 index 0000000000000..7ba929c089c6b --- /dev/null +++ b/crates/oxc_transformer/src/plugins/styled_components.rs @@ -0,0 +1,1154 @@ +//! Styled Components +//! +//! This plugin adds support for server-side rendering, minification of styles, and +//! a nicer debugging experience when using styled-components. +//! +//! > This plugin is port from the official Babel plugin for styled-components. +//! +//! ## Implementation Status +//! +//! > Note: Currently, this plugin only supports styled-components imported via import statements. +//! The transformation will not be applied if you import it using `require("styled-components")`, +//! in other words, it only supports `ESM` not `CJS`. +//! +//! ### Options: +//! **✅ Fully Supported:** +//! - `displayName`: Adds display names for debugging +//! - `fileName`: Controls filename prefixing in display names +//! - `ssr`: Adds unique component IDs for server-side rendering +//! - `transpileTemplateLiterals`: Converts template literals to function calls +//! - `minify`: Minifies CSS content in template literals +//! - `namespace`: Adds namespace prefixes to component IDs +//! - `meaninglessFileNames`: Controls which filenames are considered meaningless +//! +//! **⚠️ Partially Supported:** +//! - `pure`: Only supports call expressions, not tagged template expressions (bundler limitation) +//! +//! **❌ Not Yet Implemented:** +//! - `cssProp`: JSX css prop transformation +//! - `topLevelImportPaths`: Custom import path handling +//! +//! ## Example +//! +//! Input: +//! ```js +//! import styled from 'styled-components'; +//! +//! const Button = styled.div` +//! color: blue; +//! padding: 10px; +//! `; +//! ``` +//! +//! Output (with default options): +//! ```js +//! import styled from 'styled-components'; +//! +//! const Button = styled.div.withConfig({ +//! displayName: "Button", +//! componentId: "sc-1234567-0" +//! })(["color:blue;padding:10px;"]); +//! ``` +//! +//! ## References +//! +//! - Babel plugin: +//! - Documentation: +#![expect(clippy::doc_link_with_quotes)] +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::Deserialize; + +use std::iter::once; + +use oxc_allocator::{StringBuilder, TakeIn, Vec as ArenaVec}; +use oxc_ast::{AstBuilder, NONE, ast::*}; +use oxc_semantic::SymbolId; +use oxc_span::SPAN; +use oxc_traverse::{Ancestor, Traverse}; + +use crate::{ + context::{TransformCtx, TraverseCtx}, + state::TransformState, +}; + +#[derive(Debug, Clone, Deserialize)] +#[serde(default, rename_all = "camelCase", deny_unknown_fields)] +pub struct StyledComponentsOptions { + /// Enhances the attached CSS class name on each component with richer output to help + /// identify your components in the DOM without React DevTools. It also allows you to + /// see the component's `displayName` in React DevTools. + /// + /// When enabled, components show up as `