diff --git a/.changeset/expose-html-parser-vue-option.md b/.changeset/expose-html-parser-vue-option.md
new file mode 100644
index 000000000000..cabdc09382b3
--- /dev/null
+++ b/.changeset/expose-html-parser-vue-option.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": minor
+---
+
+Exposed the `html.parser.vue` configuration option. This option enables parsing of Vue syntax (directives like `v-if`, `v-bind`, etc.) in `.html` files. Most Vue users don't need to enable this option since Vue files typically use the `.vue` extension, but it can be useful for projects that embed Vue syntax in regular HTML files.
diff --git a/crates/biome_cli/tests/cases/html.rs b/crates/biome_cli/tests/cases/html.rs
index 517f1e6dc699..4ae300f76032 100644
--- a/crates/biome_cli/tests/cases/html.rs
+++ b/crates/biome_cli/tests/cases/html.rs
@@ -457,6 +457,92 @@ fn no_undeclared_classes_silent_without_style_info() {
));
}
+#[test]
+fn should_error_when_vue_is_disabled() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ let html_file = Utf8Path::new("file.html");
+ fs.insert(
+ html_file.into(),
+ r#"
{{ message }}
+"#
+ .as_bytes(),
+ );
+ fs.insert(
+ Utf8Path::new("biome.json").into(),
+ r#"{
+ "html": {
+ "linter": {
+ "enabled": true
+ }
+ }
+}"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", html_file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_err(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "should_error_when_vue_is_disabled",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn should_not_error_when_vue_is_enabled() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ let html_file = Utf8Path::new("file.html");
+ fs.insert(
+ html_file.into(),
+ r#"{{ message }}
+"#
+ .as_bytes(),
+ );
+ fs.insert(
+ Utf8Path::new("biome.json").into(),
+ r#"{
+ "html": {
+ "parser": {
+ "vue": true,
+ "interpolation": true
+ },
+ "linter": {
+ "enabled": true
+ }
+ }
+}"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", html_file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "should_not_error_when_vue_is_enabled",
+ fs,
+ console,
+ result,
+ ));
+}
+
#[test]
fn should_lint_a_html_file() {
let fs = MemoryFileSystem::default();
diff --git a/crates/biome_cli/tests/snapshots/main_cases_html/should_error_when_vue_is_disabled.snap b/crates/biome_cli/tests/snapshots/main_cases_html/should_error_when_vue_is_disabled.snap
new file mode 100644
index 000000000000..a4abfde44bb0
--- /dev/null
+++ b/crates/biome_cli/tests/snapshots/main_cases_html/should_error_when_vue_is_disabled.snap
@@ -0,0 +1,68 @@
+---
+source: crates/biome_cli/tests/snap_test.rs
+expression: redactor(content)
+---
+## `biome.json`
+
+```json
+{
+ "html": {
+ "linter": {
+ "enabled": true
+ }
+ }
+}
+```
+
+## `file.html`
+
+```html
+{{ message }}
+
+```
+
+# Termination Message
+
+```block
+lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ × Some errors were emitted while running checks.
+
+
+
+```
+
+# Emitted Messages
+
+```block
+file.html:1:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ × Vue syntax isn't enabled. Is this supposed to be a .vue file?
+
+ > 1 │ {{ message }}
+ │ ^^^^^^^^^^^
+ 2 │
+
+ i Remove it or enable the parsing using the html.parser.vue option.
+
+
+```
+
+```block
+file.html:1:18 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ × Text expressions aren't supported.
+
+ > 1 │ {{ message }}
+ │ ^^^^^^^^^^^^^
+ 2 │
+
+ i Remove it or enable the parsing using the html.parser.interpolation option.
+
+
+```
+
+```block
+Checked 1 file in . No fixes applied.
+Found 2 errors.
+```
diff --git a/crates/biome_cli/tests/snapshots/main_cases_html/should_not_error_when_vue_is_enabled.snap b/crates/biome_cli/tests/snapshots/main_cases_html/should_not_error_when_vue_is_enabled.snap
new file mode 100644
index 000000000000..fbb25f052bb6
--- /dev/null
+++ b/crates/biome_cli/tests/snapshots/main_cases_html/should_not_error_when_vue_is_enabled.snap
@@ -0,0 +1,32 @@
+---
+source: crates/biome_cli/tests/snap_test.rs
+expression: redactor(content)
+---
+## `biome.json`
+
+```json
+{
+ "html": {
+ "parser": {
+ "vue": true,
+ "interpolation": true
+ },
+ "linter": {
+ "enabled": true
+ }
+ }
+}
+```
+
+## `file.html`
+
+```html
+{{ message }}
+
+```
+
+# Emitted Messages
+
+```block
+Checked 1 file in . No fixes applied.
+```
diff --git a/crates/biome_configuration/src/html.rs b/crates/biome_configuration/src/html.rs
index 485d4a244948..e3d07c80ca40 100644
--- a/crates/biome_configuration/src/html.rs
+++ b/crates/biome_configuration/src/html.rs
@@ -48,6 +48,7 @@ pub type HtmlFormatterEnabled = Bool; // Keep it disabled by default whil
pub type HtmlLinterEnabled = Bool;
pub type HtmlAssistEnabled = Bool;
pub type HtmlParseInterpolation = Bool;
+pub type HtmlParseVue = Bool;
/// Options that changes how the HTML parser behaves
#[derive(
@@ -58,6 +59,13 @@ pub type HtmlParseInterpolation = Bool;
pub struct HtmlParserConfiguration {
/// Enables the parsing of double text expressions such as `{{ expression }}` inside `.html` files
pub interpolation: Option,
+
+ /// Enables parsing of Vue syntax (v-if, v-bind, etc.) in `.html` files.
+ ///
+ /// Biome will already automatically enable Vue parsing in `.vue` files, so you probably don't want
+ /// to enable this option. This only affects `.html` files, and does not change how `.vue`, `.svelte`,
+ /// or `.astro` files are parsed.
+ pub vue: Option,
}
/// Options that changes how the HTML formatter behaves
diff --git a/crates/biome_html_parser/src/parser.rs b/crates/biome_html_parser/src/parser.rs
index 4c14725d4646..4c952a021d00 100644
--- a/crates/biome_html_parser/src/parser.rs
+++ b/crates/biome_html_parser/src/parser.rs
@@ -153,6 +153,10 @@ impl HtmlParserOptions {
self
}
+ pub fn set_vue(&mut self, value: bool) {
+ self.vue = value;
+ }
+
pub fn with_svelte(mut self) -> Self {
self.svelte = true;
self
diff --git a/crates/biome_service/src/file_handlers/html.rs b/crates/biome_service/src/file_handlers/html.rs
index 10ca0f56fa81..b545faa4e7e5 100644
--- a/crates/biome_service/src/file_handlers/html.rs
+++ b/crates/biome_service/src/file_handlers/html.rs
@@ -24,7 +24,8 @@ use crate::{
use biome_analyze::{AnalysisFilter, AnalyzerConfiguration, AnalyzerOptions, ControlFlow, Never};
use biome_configuration::html::{
HtmlAssistConfiguration, HtmlAssistEnabled, HtmlFormatterConfiguration, HtmlFormatterEnabled,
- HtmlLinterConfiguration, HtmlLinterEnabled, HtmlParseInterpolation, HtmlParserConfiguration,
+ HtmlLinterConfiguration, HtmlLinterEnabled, HtmlParseInterpolation, HtmlParseVue,
+ HtmlParserConfiguration,
};
use biome_css_parser::{CssModulesKind, parse_css_with_offset_and_cache};
use biome_css_syntax::{
@@ -72,12 +73,14 @@ use tracing::{debug_span, error, instrument, trace_span};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct HtmlParserSettings {
pub interpolation: Option,
+ pub vue: Option,
}
impl From for HtmlParserSettings {
fn from(configuration: HtmlParserConfiguration) -> Self {
Self {
interpolation: configuration.interpolation,
+ vue: configuration.vue,
}
}
}
@@ -326,6 +329,9 @@ impl ServiceLanguage for HtmlLanguage {
if language.interpolation.unwrap_or_default().into() && html_file_source.is_html() {
options = options.with_double_text_expression();
}
+ if language.vue.unwrap_or_default().into() && html_file_source.is_html() {
+ options = options.with_vue();
+ }
overrides.apply_override_html_parser_options(path, &mut options);
diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs
index 6e2fc4b56852..be82fe2b7668 100644
--- a/crates/biome_service/src/settings.rs
+++ b/crates/biome_service/src/settings.rs
@@ -1879,6 +1879,11 @@ impl OverrideSettingPattern {
if let Some(interpolation) = html_parser.interpolation {
options.set_double_text_expression(interpolation.value());
}
+ if options.is_html() {
+ if let Some(vue) = html_parser.vue {
+ options.set_vue(vue.value());
+ }
+ }
}
fn apply_overrides_to_css_parser_options(&self, options: &mut CssParserOptions) {
diff --git a/crates/biome_service/src/settings.tests.rs b/crates/biome_service/src/settings.tests.rs
index 5dbf4bf91877..a87ec6730dc8 100644
--- a/crates/biome_service/src/settings.tests.rs
+++ b/crates/biome_service/src/settings.tests.rs
@@ -6,6 +6,7 @@ use crate::settings::{
use crate::workspace::DocumentFileSource;
use biome_analyze::RuleFilter;
use biome_configuration::analyzer::{GroupPlainConfiguration, SeverityOrGroup, Style};
+use biome_configuration::html::{HtmlConfiguration, HtmlParserConfiguration};
use biome_configuration::javascript::JsxRuntime;
use biome_configuration::json::{JsonAssistConfiguration, JsonLinterConfiguration};
use biome_configuration::max_size::MaxSize;
@@ -15,6 +16,8 @@ use biome_configuration::{
Overrides, RuleConfiguration, RulePlainConfiguration, Rules,
};
use biome_fs::BiomePath;
+use biome_html_parser::{HtmlParserOptions, parse_html};
+use biome_html_syntax::{HtmlFileSource, HtmlLanguage};
use biome_js_syntax::JsLanguage;
use camino::{Utf8Path, Utf8PathBuf};
use rustc_hash::FxHashSet;
@@ -236,6 +239,50 @@ fn override_inherits_global_formatter_when_not_specified() {
);
}
+#[test]
+fn html_parser_vue_only_applies_to_html_files() {
+ let configuration = Configuration {
+ html: Some(HtmlConfiguration {
+ parser: Some(HtmlParserConfiguration {
+ vue: Some(true.into()),
+ ..HtmlParserConfiguration::default()
+ }),
+ ..HtmlConfiguration::default()
+ }),
+ ..Default::default()
+ };
+
+ let mut settings = Settings::default();
+ settings
+ .merge_with_configuration(configuration, None, vec![])
+ .expect("valid configuration");
+
+ let language = HtmlLanguage::lookup_settings(&settings.languages);
+
+ let html_options = resolve_html_parse_options(
+ &settings,
+ &language.parser,
+ Utf8Path::new("file.html"),
+ HtmlFileSource::html(),
+ );
+ let astro_options = resolve_html_parse_options(
+ &settings,
+ &language.parser,
+ Utf8Path::new("file.astro"),
+ HtmlFileSource::astro(),
+ );
+ let svelte_options = resolve_html_parse_options(
+ &settings,
+ &language.parser,
+ Utf8Path::new("file.svelte"),
+ HtmlFileSource::svelte(),
+ );
+
+ assert_no_parse_diagnostics("
", html_options);
+ assert_has_parse_diagnostics("
", astro_options);
+ assert_has_parse_diagnostics("
", svelte_options);
+}
+
#[test]
fn test_module_graph_resolution_kind_from_scan_kind() {
// Test all ScanKind variants map to correct ModuleGraphResolutionKind
@@ -295,3 +342,34 @@ fn test_project_scan_disables_module_graph_type_inference() {
"Project scan should NOT enable type inference"
);
}
+
+fn resolve_html_parse_options(
+ settings: &Settings,
+ parser: &::ParserSettings,
+ path: &Utf8Path,
+ file_source: HtmlFileSource,
+) -> HtmlParserOptions {
+ HtmlLanguage::resolve_parse_options(
+ &settings.override_settings,
+ parser,
+ &BiomePath::new(Utf8PathBuf::from(path)),
+ &DocumentFileSource::from(file_source),
+ )
+}
+
+fn assert_has_parse_diagnostics(source: &str, options: HtmlParserOptions) {
+ let parse = parse_html(source, options);
+ assert!(
+ !parse.diagnostics().is_empty(),
+ "expected parsing to fail, but it succeeded"
+ );
+}
+
+fn assert_no_parse_diagnostics(source: &str, options: HtmlParserOptions) {
+ let parse = parse_html(source, options);
+ assert!(
+ parse.diagnostics().is_empty(),
+ "expected parsing to succeed, got diagnostics: {:?}",
+ parse.diagnostics()
+ );
+}
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 0edcd61bb2d9..db23afa2b43a 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -697,6 +697,14 @@ export interface HtmlParserConfiguration {
* Enables the parsing of double text expressions such as `{{ expression }}` inside `.html` files
*/
interpolation?: Bool;
+ /**
+ * Enables parsing of Vue syntax (v-if, v-bind, etc.) in `.html` files.
+
+Biome will already automatically enable Vue parsing in `.vue` files, so you probably don't want
+to enable this option. This only affects `.html` files, and does not change how `.vue`, `.svelte`,
+or `.astro` files are parsed.
+ */
+ vue?: Bool;
}
/**
* Assist options specific to the JavaScript assist
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 793628b914e7..8588c962af53 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -2083,6 +2083,10 @@
"interpolation": {
"description": "Enables the parsing of double text expressions such as `{{ expression }}` inside `.html` files",
"anyOf": [{ "$ref": "#/$defs/Bool" }, { "type": "null" }]
+ },
+ "vue": {
+ "description": "Enables parsing of Vue syntax (v-if, v-bind, etc.) in `.html` files.\n\nBiome will already automatically enable Vue parsing in `.vue` files, so you probably don't want\nto enable this option. This only affects `.html` files, and does not change how `.vue`, `.svelte`,\nor `.astro` files are parsed.",
+ "anyOf": [{ "$ref": "#/$defs/Bool" }, { "type": "null" }]
}
},
"additionalProperties": false