Skip to content

Commit 96d09f4

Browse files
baeseokjaedyc3
andauthored
feat(lint): add noVueOptionsApi rule (#8648)
Co-authored-by: Carson McManus <dyc3@users.noreply.github.com>
1 parent 0d15370 commit 96d09f4

File tree

57 files changed

+1293
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1293
-1
lines changed

.changeset/add-no-v-options-api.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added a new nursery rule [`noVueOptionsApi`](https://biomejs.dev/linter/rules/no-vue-options-api/).
6+
7+
Biome now reports Vue Options API usage, which is incompatible with Vue 3.6's Vapor Mode.
8+
This rule detects Options API patterns in `<script>` blocks, `defineComponent()`, and `createApp()` calls,
9+
helping prepare codebases for Vapor Mode adoption.
10+
11+
For example, the following now triggers this rule:
12+
13+
```vue
14+
<script>
15+
export default {
16+
data() {
17+
return { count: 0 };
18+
}
19+
}
20+
</script>
21+
```

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_html_analyze/src/lint/nursery/use_vue_vapor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ declare_lint_rule! {
3535
/// </script>
3636
/// ```
3737
///
38+
/// ## Related Rules
39+
///
40+
/// - [noVueOptionsApi](https://biomejs.dev/linter/rules/no-vue-options-api): Disallows the Options API format, which is incompatible with Vapor Mode
41+
///
3842
pub UseVueVapor {
3943
version: "2.3.11",
4044
name: "useVueVapor",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub mod no_useless_catch_binding;
4040
pub mod no_useless_undefined;
4141
pub mod no_vue_data_object_declaration;
4242
pub mod no_vue_duplicate_keys;
43+
pub mod no_vue_options_api;
4344
pub mod no_vue_reserved_keys;
4445
pub mod no_vue_reserved_props;
4546
pub mod no_vue_setup_props_reactivity_loss;
@@ -60,4 +61,4 @@ pub mod use_spread;
6061
pub mod use_vue_consistent_define_props_declaration;
6162
pub mod use_vue_define_macros_order;
6263
pub mod use_vue_multi_word_component_names;
63-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
64+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
use biome_analyze::{Rule, RuleDiagnostic, RuleDomain, context::RuleContext, declare_lint_rule};
2+
use biome_console::markup;
3+
use biome_diagnostics::Severity;
4+
use biome_js_syntax::{AnyJsExpression, JsFileSource};
5+
use biome_rowan::{AstNode, TextRange};
6+
use biome_rule_options::no_vue_options_api::NoVueOptionsApiOptions;
7+
8+
use crate::frameworks::vue::vue_call::is_vue_api_reference;
9+
use crate::frameworks::vue::vue_component::{
10+
AnyVueComponent, VueComponent, VueComponentQuery, VueOptionsApiBasedComponent,
11+
};
12+
13+
declare_lint_rule! {
14+
/// Disallow the use of Vue Options API.
15+
///
16+
/// Vue 3.6's Vapor Mode does not support the Options API.
17+
/// Components must use the Composition API (`<script setup>` or `defineComponent` with function signature) instead.
18+
///
19+
/// This rule helps prepare codebases for Vapor Mode by detecting Options API
20+
/// patterns that are incompatible with the new rendering mode.
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// ```vue,expect_diagnostic
27+
/// <script>
28+
/// export default {
29+
/// data() {
30+
/// return { count: 0 }
31+
/// }
32+
/// }
33+
/// </script>
34+
/// ```
35+
///
36+
/// ```vue,expect_diagnostic
37+
/// <script>
38+
/// export default {
39+
/// methods: {
40+
/// increment() {
41+
/// this.count++
42+
/// }
43+
/// }
44+
/// }
45+
/// </script>
46+
/// ```
47+
///
48+
/// ```vue,expect_diagnostic
49+
/// <script>
50+
/// export default {
51+
/// computed: {
52+
/// doubled() {
53+
/// return this.count * 2
54+
/// }
55+
/// }
56+
/// }
57+
/// </script>
58+
/// ```
59+
///
60+
/// ```vue,expect_diagnostic
61+
/// <script>
62+
/// export default {
63+
/// mounted() {
64+
/// console.log('Component mounted')
65+
/// }
66+
/// }
67+
/// </script>
68+
/// ```
69+
///
70+
/// ```js,expect_diagnostic
71+
/// import { defineComponent } from 'vue'
72+
///
73+
/// defineComponent({
74+
/// name: 'MyComponent',
75+
/// data() {
76+
/// return { count: 0 }
77+
/// }
78+
/// })
79+
/// ```
80+
///
81+
/// ### Valid
82+
///
83+
/// ```vue
84+
/// <script setup>
85+
/// import { ref } from 'vue'
86+
/// const count = ref(0)
87+
/// </script>
88+
/// ```
89+
///
90+
/// ```vue
91+
/// <script setup>
92+
/// import { ref, computed } from 'vue'
93+
///
94+
/// const count = ref(0)
95+
/// const doubled = computed(() => count.value * 2)
96+
/// </script>
97+
/// ```
98+
///
99+
/// ```vue
100+
/// <script setup>
101+
/// import { onMounted } from 'vue'
102+
///
103+
/// onMounted(() => {
104+
/// console.log('Component mounted')
105+
/// })
106+
/// </script>
107+
/// ```
108+
///
109+
/// ## Related Rules
110+
///
111+
/// - [useVueVapor](https://biomejs.dev/linter/rules/use-vue-vapor): Enforces the use of Vapor mode in Vue components
112+
///
113+
/// ## Resources
114+
///
115+
/// - [Vue 3 Composition API](https://vuejs.org/api/composition-api-setup.html)
116+
/// - [Options API vs Composition API](https://vuejs.org/guide/introduction.html#api-styles)
117+
///
118+
pub NoVueOptionsApi {
119+
version: "next",
120+
name: "noVueOptionsApi",
121+
language: "js",
122+
recommended: false,
123+
severity: Severity::Error,
124+
domains: &[RuleDomain::Vue],
125+
}
126+
}
127+
128+
/// State for detected Options API component.
129+
pub struct RuleState {
130+
range: TextRange,
131+
}
132+
133+
impl Rule for NoVueOptionsApi {
134+
type Query = VueComponentQuery;
135+
type State = RuleState;
136+
type Signals = Option<Self::State>;
137+
type Options = NoVueOptionsApiOptions;
138+
139+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
140+
let component = VueComponent::from_potential_component(
141+
ctx.query(),
142+
ctx.model(),
143+
ctx.source_type::<JsFileSource>(),
144+
ctx.file_path(),
145+
)?;
146+
147+
// <script setup> or defineComponent with function signature are valid for Vapor Mode
148+
match component.kind() {
149+
AnyVueComponent::Setup(_) => None,
150+
AnyVueComponent::OptionsApi(opts) => {
151+
let expr = opts.definition_expression()?;
152+
if is_define_component_or_create_app(&expr, ctx.model()) {
153+
return None;
154+
}
155+
Some(RuleState {
156+
range: expr.range(),
157+
})
158+
}
159+
AnyVueComponent::DefineComponent(component) => {
160+
if component.setup_func().is_some() {
161+
return None;
162+
}
163+
let definition = component.definition_expression()?;
164+
if definition.as_js_object_expression().is_some() {
165+
Some(RuleState {
166+
range: definition.range(),
167+
})
168+
} else {
169+
None
170+
}
171+
}
172+
AnyVueComponent::CreateApp(component) => {
173+
let definition = component.definition_expression()?;
174+
if definition.as_js_object_expression().is_some() {
175+
Some(RuleState {
176+
range: definition.range(),
177+
})
178+
} else {
179+
None
180+
}
181+
}
182+
}
183+
}
184+
185+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
186+
Some(
187+
RuleDiagnostic::new(
188+
rule_category!(),
189+
state.range,
190+
markup! {
191+
"Using the Options API is not allowed."
192+
},
193+
)
194+
.note(markup! {
195+
"Although the Options API is still supported by Vue, using the Composition API is recommended, and makes it possible to use Vue's Vapor mode for better performance."
196+
})
197+
.note(markup! {
198+
"Use "<Emphasis>"<script setup>"</Emphasis>" or "<Emphasis>"defineComponent"</Emphasis>" with a function signature to use the "<Hyperlink href="https://vuejs.org/guide/introduction.html#composition-api">"Composition API"</Hyperlink>" instead."
199+
}),
200+
)
201+
}
202+
}
203+
204+
fn is_define_component_or_create_app(
205+
expr: &AnyJsExpression,
206+
model: &biome_js_semantic::SemanticModel,
207+
) -> bool {
208+
let Some(call_expr) = expr.as_js_call_expression() else {
209+
return false;
210+
};
211+
let Some(callee) = call_expr.callee().ok().and_then(|c| c.inner_expression()) else {
212+
return false;
213+
};
214+
is_vue_api_reference(&callee, model, "defineComponent")
215+
|| is_vue_api_reference(&callee, model, "createApp")
216+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
// should generate diagnostics
3+
// createApp with Options API is not supported in Vapor Mode
4+
import { createApp } from "vue";
5+
6+
createApp({
7+
data() {
8+
return { count: 0 };
9+
},
10+
}).mount("#app");
11+
</script>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid-createapp-data.vue
4+
---
5+
# Input
6+
```ts
7+
// should generate diagnostics
8+
// createApp with Options API is not supported in Vapor Mode
9+
import { createApp } from "vue";
10+
11+
createApp({
12+
data() {
13+
return { count: 0 };
14+
},
15+
}).mount("#app");
16+
17+
```
18+
19+
# Diagnostics
20+
```
21+
invalid-createapp-data.vue:5:11 lint/nursery/noVueOptionsApi ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
22+
23+
× Using the Options API is not allowed.
24+
25+
3 │ import { createApp } from "vue";
26+
4 │
27+
> 5 │ createApp({
28+
^
29+
> 6data() {
30+
> 7 │ return { count: 0 };
31+
> 8},
32+
> 9 │ }).mount("#app");
33+
│ ^
34+
10 │
35+
36+
i Although the Options API is still supported by Vue, using the Composition API is recommended, and makes it possible to use Vue's Vapor mode for better performance.
37+
38+
i Use <script setup> or defineComponent with a function signature to use the Composition API instead.
39+
40+
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.
41+
42+
43+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// should generate diagnostics
2+
// createApp with empty object is Options API style
3+
import { createApp } from "vue";
4+
5+
createApp({}).mount("#app");

0 commit comments

Comments
 (0)