Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-unsupported-script-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9506](https://github.com/biomejs/biome/issues/9506) and [#9479](https://github.com/biomejs/biome/issues/9479): Biome no longer reports false parse errors on `<script type="speculationrules">` and `<script type="application/ld+json">` tags. These script types contain non-JavaScript content and are now correctly skipped by the embedded language detector.
26 changes: 16 additions & 10 deletions crates/biome_service/src/embed/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,29 @@ static HTML_DETECTORS: [EmbedDetector; 5] = [
// <script> → JS/TS/JSON (dynamic: depends on type/lang attributes + framework)
//
// A single detector handles all <script> variants via the dynamic resolver:
// - <script> → JsScript (classic HTML)
// - <script type="module"> → JsModule (ES module)
// - <script lang="ts"> → Ts (Vue/Svelte)
// - <script lang="tsx"> → Tsx (Vue/Svelte)
// - <script lang="jsx"> → Jsx (Vue/Svelte)
// - <script type="importmap"> → Json
// - <script type="application/json"> → Json
// - Astro <script> (no frontmatter) → Ts
// - Vue/Svelte default → JsModule
// - <script> → JsScript (classic HTML)
// - <script type="module"> → JsModule (ES module)
// - <script lang="ts"> → Ts (Vue/Svelte)
// - <script lang="tsx"> → Tsx (Vue/Svelte)
// - <script lang="jsx"> → Jsx (Vue/Svelte)
// - <script type="importmap"> → Json
// - <script type="application/json"> → Json
// - Astro <script> (no frontmatter) → Ts
// - Vue/Svelte default → JsModule
// - <script type="speculationrules"> → skipped (unsupported type)
// - <script type="application/ld+json"> → skipped (unsupported type)
// - <script type="text/x-handlebars-*"> → skipped (unsupported type)
//
// The handler dispatches on embed_match.guest to call the right parser
// (parse JS vs parse JSON). No separate detector needed for JSON scripts.
EmbedDetector::Element {
tag: "script",
target: EmbedTarget::Dynamic {
resolver: resolve_script_language,
fallback: Some(GuestLanguage::JsScript),
// No fallback: the resolver handles all supported cases explicitly.
// Returning None means "skip this element" (e.g. unsupported type
// like speculationrules or application/ld+json).
fallback: None,
},
},
// <style> → CSS (dynamic: skips SCSS via resolver returning None)
Expand Down
77 changes: 77 additions & 0 deletions crates/biome_service/src/workspace/server.tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,83 @@ fn pull_diagnostics_and_actions_for_js_file() {
insta::assert_debug_snapshot!(result)
}

/// Regression test for https://github.com/biomejs/biome/issues/9506 and
/// https://github.com/biomejs/biome/issues/9479.
///
/// `<script type="speculationrules">` and `<script type="application/ld+json">`
/// contain JSON-like content that is NOT JavaScript. Before this fix, biome's
/// embed registry fallback would treat these as JavaScript, causing false
/// parse errors and incorrect lint diagnostics.
#[test]
fn no_diagnostics_for_unsupported_script_types() {
// speculationrules content is JSON-like but is NOT JavaScript.
// application/ld+json content is JSON-LD, also not JavaScript.
// Both should be silently skipped by the embed detector (no JS parse errors).
const FILE_CONTENT: &str = r#"<!doctype html>
<html>
<head>
<script type="speculationrules">
{
"prerender": [
{ "source": "list", "urls": ["/next-page"] }
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Test"
}
</script>
</head>
</html>"#;

let fs = MemoryFileSystem::default();
fs.insert(Utf8PathBuf::from("/project/file.html"), FILE_CONTENT);

let (workspace, project_key) = setup_workspace_and_open_project(fs, "/");

workspace
.scan_project(ScanProjectParams {
project_key,
watch: false,
force: false,
scan_kind: ScanKind::Project,
verbose: false,
})
.unwrap();

workspace
.open_file(OpenFileParams {
project_key,
path: BiomePath::new("/project/file.html"),
content: FileContent::FromServer,
document_file_source: None,
persist_node_cache: false,
inline_config: None,
})
.unwrap();

let result = workspace
.pull_diagnostics_and_actions(PullDiagnosticsAndActionsParams {
path: BiomePath::new("/project/file.html"),
only: vec![],
skip: vec![],
enabled_rules: vec![],
project_key,
categories: Default::default(),
inline_config: None,
})
.unwrap();

assert!(
result.diagnostics.is_empty(),
"Expected no diagnostics for unsupported script types, got: {:#?}",
result.diagnostics
);
}

#[test]
fn format_js_with_embedded_css() {
const FILE_PATH: &str = "/project/file.js";
Expand Down