Skip to content

Commit

Permalink
feat(swc-plugin-angular): ✨ optional transform styleUrl and styleUrls (
Browse files Browse the repository at this point in the history
…#421)

* feat(swc-plugin-angular): ✨ add actual styleUrls transformation

This adds replacing `styleUrls` with actual style imports instead of empty `styles: []`.
Useful when generating code for browser such as Webpack/Vite dev server.

* fix(swc-plugin-angular): 🐞 make style importing optional

This disables importing styles and keeps the old behavior that replaces `styleUrls` with empty `styles: []` by default.

Some tests don't import CSS as strings, for example Jest imports them as objects, which breaks Angular. You need to change Jest config to fix this.

To avoid making this a breaking change and force users to change existing config, style importing is disabled by default.
  • Loading branch information
56789a1987 authored Oct 14, 2024
1 parent 5703c5e commit a2dd984
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.rs]
indent_size = 4

[*.md]
max_line_length = off
trim_trailing_whitespace = false
18 changes: 17 additions & 1 deletion packages/swc-angular-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,20 @@ This means that while it is suitable for testing, it shouldn't be used to build

## Try it now with your Jest tests

Cf. [`@jscutlery/swc-angular`](../swc-angular/README.md)
Cf. [`@jscutlery/swc-angular`](../swc-angular/README.md)

## Config

```ts
[
'@jscutlery/swc-angular-plugin',
{
// this plugin removes styles by default, enable this to transform style urls to style imports
importStyles?: boolean,
// add ?inline suffix to style imports for vite support
styleInlineSuffix?: boolean,
// add ?raw suffix to template imports for vite support
templateRawSuffix?: boolean,
}
]
```
70 changes: 67 additions & 3 deletions packages/swc-angular-plugin/src/component_decorator_visitor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::ops::Deref;

use swc_core::ecma::ast::{ArrayLit, Expr, Ident, Lit, ModuleItem, PropName};
use swc_core::ecma::ast::{ArrayLit, Expr, ExprOrSpread, Ident, Lit, ModuleItem, PropName};
use swc_core::ecma::visit::{VisitMut, VisitMutWith};

use crate::import_declaration::{ImportDeclaration, ImportDeclarationSpecifier};
Expand All @@ -25,6 +25,8 @@ impl ComponentDecoratorVisitor {

#[derive(Default)]
pub struct ComponentDecoratorVisitorOptions {
pub import_styles: bool,
pub style_inline_suffix: bool,
pub template_raw_suffix: bool,
}

Expand Down Expand Up @@ -74,9 +76,20 @@ impl VisitMut for ComponentDecoratorVisitor {
span: Default::default(),
optional: false,
});

node.value = Expr::Array(ArrayLit {
span: Default::default(),
elems: vec![],
elems: match self.options.import_styles {
true => {
let mut style_path = match &node.value.deref() {
Expr::Lit(Lit::Str(str)) => str.value.to_string(),
_ => return,
};

vec![self.generate_style_entry(&mut style_path)]
},
_ => vec![],
},
})
.into();
}
Expand All @@ -87,9 +100,32 @@ impl VisitMut for ComponentDecoratorVisitor {
span: Default::default(),
optional: false,
});

let mut elems = vec![];

if self.options.import_styles {
let style_paths = match &node.value.deref() {
Expr::Array(array) => &array.elems,
_ => return,
};

for path_option in style_paths.iter() {
/* Ignore non-string values in styleUrls */
let mut path = match path_option {
Some(value) => match &value.expr.deref() {
Expr::Lit(Lit::Str(str)) => str.value.to_string(),
_ => continue,
},
_ => continue,
};

elems.push(self.generate_style_entry(&mut path));
}
}

node.value = Expr::Array(ArrayLit {
span: Default::default(),
elems: vec![],
elems,
})
.into();
}
Expand Down Expand Up @@ -155,4 +191,32 @@ impl ComponentDecoratorVisitor {
self.unique_id += 1;
format!("_jsc_{name}_{unique_id}")
}

fn generate_style_entry(&mut self, path: &mut String) -> Option<ExprOrSpread> {
if !path.starts_with("./") {
path.insert_str(0, "./");
}

/* Add ?raw suffix for vite support when option is enabled. */
if self.options.style_inline_suffix {
path.push_str("?inline");
}

let style_var_name = self.generate_var_name("style");

self.imports.push(ImportDeclaration {
specifier: ImportDeclarationSpecifier::Default(style_var_name.clone()),
source: path.clone(),
});

Some(ExprOrSpread {
expr: Expr::Ident(Ident {
sym: style_var_name.into(),
span: Default::default(),
optional: Default::default(),
})
.into(),
spread: Default::default(),
})
}
}
140 changes: 111 additions & 29 deletions packages/swc-angular-plugin/src/component_decorator_visitor_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ fn test_replace_template_url() {
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrls: ['./style.css'],
templateUrl: './hello.component.html'
})
], MyCmp);"# },
Expand All @@ -25,7 +24,6 @@ fn test_replace_template_url() {
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [],
template: _jsc_template_0
})
], MyCmp);
Expand Down Expand Up @@ -78,29 +76,100 @@ fn test_replace_multiple_template_urls() {
}

#[test]
fn test_replace_style_url() {
fn test_discard_style_url() {
test_visitor(
ComponentDecoratorVisitor::default(),
indoc! {
r#"class MyCmp {}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrl: './style.css',
template: 'something'
})
], MyCmp);"# },
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrl: './style.css',
template: 'something'
})
], MyCmp);"# },
indoc! {
r#"class MyCmp {
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [],
template: 'something'
})
], MyCmp);
"# },
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [],
template: 'something'
})
], MyCmp);
"# },
);
}

#[test]
fn test_replace_style_url() {
test_visitor(
ComponentDecoratorVisitor::new(ComponentDecoratorVisitorOptions {
import_styles: true,
style_inline_suffix: false,
template_raw_suffix: false,
}),
indoc! {
r#"class MyCmp {}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrl: './style.css',
template: 'something'
})
], MyCmp);"# },
indoc! {
r#"import _jsc_style_0 from "./style.css";
class MyCmp {
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [
_jsc_style_0
],
template: 'something'
})
], MyCmp);
"# },
);
}

#[test]
fn test_replace_style_urls() {
test_visitor(
ComponentDecoratorVisitor::new(ComponentDecoratorVisitorOptions {
import_styles: true,
style_inline_suffix: false,
template_raw_suffix: false,
}),
indoc! {
r#"class MyCmp {}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrls: ['./style1.css', './style2.css'],
templateUrl: './hello.component.html'
})
], MyCmp);"# },
indoc! {
r#"import _jsc_style_0 from "./style1.css";
import _jsc_style_1 from "./style2.css";
import _jsc_template_2 from "./hello.component.html";
class MyCmp {
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [
_jsc_style_0,
_jsc_style_1
],
template: _jsc_template_2
})
], MyCmp);
"# },
);
}

Expand Down Expand Up @@ -143,56 +212,69 @@ fn test_replace_urls_in_component_decorator_only() {
}

#[test]
fn test_append_relative_path_to_template_url() {
fn test_append_relative_path_to_template_and_style_url() {
test_visitor(
ComponentDecoratorVisitor::default(),
ComponentDecoratorVisitor::new(ComponentDecoratorVisitorOptions {
import_styles: true,
style_inline_suffix: false,
template_raw_suffix: false,
}),
indoc! {
r#"class MyCmp {}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrl: 'style.css',
templateUrl: 'hello.component.html'
})
], MyCmp);"# },
indoc! {
r#"
import _jsc_template_0 from "./hello.component.html";
r#"import _jsc_style_0 from "./style.css";
import _jsc_template_1 from "./hello.component.html";
class MyCmp {
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
template: _jsc_template_0
styles: [
_jsc_style_0
],
template: _jsc_template_1
})
], MyCmp);
"# },
);
}

#[test]
fn test_add_raw_query_to_template_import() {
fn test_add_raw_query_to_template_and_style_import() {
test_visitor(
ComponentDecoratorVisitor::new(ComponentDecoratorVisitorOptions {
import_styles: true,
style_inline_suffix: true,
template_raw_suffix: true,
}),
indoc! {
r#"class MyCmp {}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styleUrls: ['./style.css'],
styleUrl: './style.css',
templateUrl: './hello.component.html'
})
], MyCmp);"# },
indoc! {
r#"import _jsc_template_0 from "./hello.component.html?raw";
r#"import _jsc_style_0 from "./style.css?inline";
import _jsc_template_1 from "./hello.component.html?raw";
class MyCmp {
}
MyCmp = _ts_decorate([
Component({
selector: 'app-hello',
styles: [],
template: _jsc_template_0
styles: [
_jsc_style_0
],
template: _jsc_template_1
})
], MyCmp);
"# },
Expand Down
14 changes: 14 additions & 0 deletions packages/swc-angular-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,27 @@ pub fn process_transform(program: Program, metadata: TransformPluginProgramMetad
serde_json::from_str(config_str.as_str())
.expect("Invalid @jscutlery/swc-angular-plugin config")
});

let import_styles = config
.as_ref()
.and_then(|value| value["importStyles"].as_bool())
.unwrap_or_default();

let style_inline_suffix = config
.as_ref()
.and_then(|value| value["styleInlineSuffix"].as_bool())
.unwrap_or_default();

let template_raw_suffix = config
.as_ref()
.and_then(|value| value["templateRawSuffix"].as_bool())
.unwrap_or_default();

program
.fold_with(&mut as_folder(ComponentDecoratorVisitor::new(
ComponentDecoratorVisitorOptions {
import_styles,
style_inline_suffix,
template_raw_suffix,
},
)))
Expand Down
Loading

0 comments on commit a2dd984

Please sign in to comment.