diff --git a/.changeset/add-use-vue-scoped-styles.md b/.changeset/add-use-vue-scoped-styles.md new file mode 100644 index 000000000000..4cb04d389f6c --- /dev/null +++ b/.changeset/add-use-vue-scoped-styles.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule `useVueScopedStyles` for Vue SFCs. This rule enforces that ` + /// ``` + /// + /// ```astro,expect_diagnostic + /// + /// ``` + /// + /// ### Valid + /// + /// ```vue + /// + /// ``` + /// + /// ```vue + /// + /// ``` + /// + /// ## References: + /// + /// - [Vue Documentation](https://vuejs.org/api/sfc-css-features.html#scoped-css) + /// - [Astro Documentation](https://docs.astro.build/en/guides/styling/#global-styles) + pub UseScopedStyles { + version: "next", + name: "useScopedStyles", + language: "html", + recommended: true, + domains: &[RuleDomain::Vue], + sources: &[RuleSource::EslintVueJs("enforce-style-attribute").inspired()], + fix_kind: FixKind::Unsafe, + } +} + +pub enum GlobalStylesKind { + Vue, + Astro { directive: AstroIsDirective }, +} + +impl Rule for UseScopedStyles { + type Query = Ast; + type State = GlobalStylesKind; + type Signals = Option; + type Options = UseScopedStylesOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + if !ctx.source_type::().is_vue() + && !ctx.source_type::().is_astro() + { + return None; + } + + let opening = ctx.query(); + + let name = opening.name().ok()?; + let name_text = name.token_text_trimmed()?; + if !name_text.eq_ignore_ascii_case("style") { + return None; + } + + let attributes = opening.attributes(); + if ctx.source_type::().is_vue() { + let has_scoped = attributes.find_by_name("scoped").is_some(); + let has_module = attributes.find_by_name("module").is_some(); + + if has_scoped || has_module { + return None; + } else { + return Some(GlobalStylesKind::Vue); + } + } else if ctx.source_type::().is_astro() { + let is_directives = attributes + .iter() + .filter_map(|attr| attr.syntax().clone().cast::()); + for directive in is_directives { + let name = directive.value().ok()?.name().ok()?; + let name_text = name.token_text_trimmed()?; + if name_text.eq_ignore_ascii_case("global") { + return Some(GlobalStylesKind::Astro { directive }); + } + } + return None; + } + + None + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + match state { + GlobalStylesKind::Vue => { + Some( + RuleDiagnostic::new( + rule_category!(), + ctx.query().range(), + markup! { + "This "" diff --git a/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro.snap b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro.snap new file mode 100644 index 000000000000..6c864b70f0f2 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.astro.snap @@ -0,0 +1,39 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.astro +--- +# Input +```html + + + + +``` + +# Diagnostics +``` +invalid.astro:3:8 lint/nursery/useScopedStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This is:global directive is making the styles in this block global. + + 1 │ + 2 │ + > 3 │ + + diff --git a/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue.snap b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue.snap new file mode 100644 index 000000000000..4a88be56b103 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/invalid.vue.snap @@ -0,0 +1,64 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: invalid.vue +--- +# Input +```html + + + + + + +``` + +# Diagnostics +``` +invalid.vue:3:1 lint/nursery/useScopedStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This + + i In Vue, unscoped styles become global across the entire project. This can lead to unintended side effects and maintenance challenges. Adding the scoped attribute ensures that styles are scoped to this component, preventing style leakage and conflicts. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Unsafe fix: Add the scoped attribute so the styles will only apply to this component. + + 3 │ + │ +++++++ + +``` + +``` +invalid.vue:7:1 lint/nursery/useScopedStyles FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i This + 6 │ + > 7 │ + + i In Vue, unscoped styles become global across the entire project. This can lead to unintended side effects and maintenance challenges. Adding the scoped attribute ensures that styles are scoped to this component, preventing style leakage and conflicts. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + i Unsafe fix: Add the scoped attribute so the styles will only apply to this component. + + 7 │ + │ +++++++ + +``` diff --git a/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue new file mode 100644 index 000000000000..f93ef2ce3c8b --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue.snap b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue.snap new file mode 100644 index 000000000000..5c917b03d434 --- /dev/null +++ b/crates/biome_html_analyze/tests/specs/nursery/useScopedStyles/valid.vue.snap @@ -0,0 +1,25 @@ +--- +source: crates/biome_html_analyze/tests/spec_tests.rs +expression: valid.vue +--- +# Input +```html + + + + + + + + + + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index c40014efc7d8..3087ef0b6e25 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -403,6 +403,7 @@ pub mod use_readonly_class_properties; pub mod use_regex_literals; pub mod use_regexp_exec; pub mod use_required_scripts; +pub mod use_scoped_styles; pub mod use_self_closing_elements; pub mod use_semantic_elements; pub mod use_shorthand_assign; diff --git a/crates/biome_rule_options/src/use_scoped_styles.rs b/crates/biome_rule_options/src/use_scoped_styles.rs new file mode 100644 index 000000000000..dc63d037b0d4 --- /dev/null +++ b/crates/biome_rule_options/src/use_scoped_styles.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct UseScopedStylesOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index b136c4586425..236a13e665bb 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2410,6 +2410,11 @@ See https://biomejs.dev/linter/rules/use-required-scripts */ useRequiredScripts?: UseRequiredScriptsConfiguration; /** + * Enforce that \