Skip to content

Conversation

@Netail
Copy link
Member

@Netail Netail commented Nov 14, 2025

Summary

Implement NextJS' noSyncScripts, but could also be valid for regular React. So made Next.js a kind of sub category, not sure if there's a better way to mention it

Closes #7647

Test Plan

Docs

@changeset-bot
Copy link

changeset-bot bot commented Nov 14, 2025

🦋 Changeset detected

Latest commit: 01b484a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Nov 14, 2025
@Netail Netail force-pushed the feat/no-sync-scripts branch from 81cda61 to 2b04613 Compare November 14, 2025 23:42
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

Adds a new nursery lint rule noSyncScripts for both HTML and JavaScript/JSX that flags <script> elements with a src attribute that lack async or defer. Implements rule logic, queries, diagnostics, and metadata in biome_html_analyze and biome_js_analyze. Adds NoSyncScriptsOptions, exposes the rules publicly, provides valid/invalid test fixtures for HTML and JSX, extends HTML syntax helpers with HtmlAttributeList::find_by_name, updates rule options exports, and adds a changeset entry. No existing public exports were removed.

Possibly related PRs

Suggested reviewers

  • dyc3
  • ematipico

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(analyze): implement noSyncScripts' directly and clearly summarises the main change: implementing the noSyncScripts lint rule.
Description check ✅ Passed The description references the implemented feature (NextJS' noSyncScripts), notes its applicability to React, and links to the corresponding issue #7647, all of which relate to the changeset.
Linked Issues check ✅ Passed The PR successfully implements the noSyncScripts rule for both JavaScript/JSX and HTML, matching the objective of porting Next.js' no-sync-scripts rule into Biome's analyser [#7647].
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the noSyncScripts rule: new rule implementations, test fixtures, configuration structures, and utility methods for attribute lookup. No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 223f06c and 01b484a.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/invalid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (2)
crates/biome_analyze/src/rule.rs (4)
  • recommended (602-605)
  • sources (617-620)
  • same (246-251)
  • domains (632-635)
crates/biome_html_analyze/src/lint/nursery/no_sync_scripts.rs (2)
  • run (44-62)
  • diagnostic (64-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: End-to-end tests
  • GitHub Check: Documentation
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test Node.js API
  • GitHub Check: Check JS Files
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Bench (biome_js_analyze)
🔇 Additional comments (7)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (7)

1-9: Imports look good.

All necessary dependencies are present for the rule implementation.


11-53: Solid rule declaration with clear documentation.

The examples effectively demonstrate both invalid and valid patterns, and the Next.js-specific guidance is a nice touch.


55-59: Union type correctly covers both element forms.


61-70: Name validation correctly distinguishes HTML elements from components.

The lowercase check ensures only <script> tags are matched, not <Script> components.


72-81: Attribute validation logic is correct.

Properly identifies scripts with src that lack async or defer.


87-87: Options type declared but currently unused.

The NoSyncScriptsOptions type is associated with the rule but isn't accessed in run() or diagnostic(). If this is reserved for future configuration, no action needed. Otherwise, consider removing it or implementing the intended options logic.


110-124: Diagnostic message is clear and actionable.

The guidance covers both general best practice and Next.js-specific alternatives, addressing the previous feedback effectively.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 14, 2025

CodSpeed Performance Report

Merging #8108 will not alter performance

Comparing Netail:feat/no-sync-scripts (01b484a) with main (f4433b3)

Summary

✅ 58 untouched
⏩ 95 skipped1

Footnotes

  1. 95 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (3)

11-52: Docs and diagnostic text are clear; one tiny wording tweak

The rule metadata and diagnostic message align well with @next/next/no-sync-scripts and explain both the problem and the fix. If you fancy polishing the copy, consider changing the docstring sentence:

A synchronous script which impact your webpage performance

to something like:

A synchronous script can impact your webpage performance

to read more naturally in English, but that’s purely cosmetic.

Also applies to: 109-121


88-107: Optional: factor out shared logic between the two match arms

Both JsxOpeningElement and JsxSelfClosingElement arms perform the same sequence of name + attributes + validation calls; you could pull that into a small helper like check_script_like(&node.name().ok()?, &node.attributes()) to reduce duplication, but it’s not critical.


61-80: Match Next.js semantics by handling JSX spread attributes

Verified: Next.js intentionally allows <script {...props} /> because spreads might supply async or defer, avoiding false positives. Your current implementation flags <script src={url} {...props} /> as synchronous since find_by_name can't see into spreads.

To align with upstream behaviour:

  • Early-return None in validate_attributes when the attribute list contains a JSX spread
  • Add a test fixture (e.g. <script src="" {...props} />) to lock this in

This keeps Biome's rule safely aligned with Next.js whilst remaining conservative.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4433b3 and 2b04613.

⛔ Files ignored due to path filters (9)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/domain_selector.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/valid.jsx.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (6)
  • .changeset/icy-bags-sleep.md (1 hunks)
  • crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/invalid.jsx (1 hunks)
  • crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/valid.jsx (1 hunks)
  • crates/biome_rule_options/src/lib.rs (1 hunks)
  • crates/biome_rule_options/src/no_sync_scripts.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (1)
crates/biome_analyze/src/rule.rs (4)
  • recommended (602-605)
  • sources (617-620)
  • same (246-251)
  • domains (632-635)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Documentation
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Check Dependencies
  • GitHub Check: Check JS Files
  • GitHub Check: Test Node.js API
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_configuration)
  • GitHub Check: Bench (biome_js_formatter)
🔇 Additional comments (5)
crates/biome_rule_options/src/no_sync_scripts.rs (1)

1-6: Options struct looks consistent

Derives and serde attributes match the usual pattern for unit options structs, so this is ready to go as-is.

crates/biome_rule_options/src/lib.rs (1)

186-186: Module export wiring is fine

The new no_sync_scripts module is correctly exposed alongside the other no_* rule options; no further changes needed.

crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/invalid.jsx (1)

1-3: Invalid case matches the rule semantics

This is a clear minimal repro for the “src without async/defer” case and should reliably trigger the diagnostic.

.changeset/icy-bags-sleep.md (1)

1-18: Changeset description is aligned and useful

The summary and JSX examples clearly describe what the rule flags and how to fix it, matching the Next.js guidance, so the metadata looks good.

crates/biome_js_analyze/tests/specs/nursery/noSyncScripts/valid.jsx (1)

1-16: Good spread of non-diagnostic cases

These fixtures exercise the key safe patterns (no src, and src with async/defer) and should keep the rule from being over-eager on common JSX.

@Netail Netail force-pushed the feat/no-sync-scripts branch from 2b04613 to 8bc4f99 Compare November 14, 2025 23:52
@Netail Netail changed the title feat(js_analyze): implement noSyncScripts feat(analyze): implement noSyncScripts Nov 15, 2025
@github-actions github-actions bot added A-Parser Area: parser L-HTML Language: HTML and super languages labels Nov 15, 2025
name: "noSyncScripts",
language: "html",
recommended: false,
sources: &[RuleSource::EslintNext("no-sync-scripts").same()],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this relevant for the html analyze? Perhaps inspired?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/biome_html_syntax/src/attr_ext.rs (1)

22-34: Consider removing redundant None.

The explicit None at line 31 is unnecessary—find_map handles non-matches when the closure simply doesn't return.

Apply this diff:

 impl HtmlAttributeList {
     pub fn find_by_name(&self, name_to_lookup: &str) -> Option<HtmlAttribute> {
         self.iter().find_map(|attribute| {
             if let AnyHtmlAttribute::HtmlAttribute(attribute) = attribute
                 && let Ok(name) = attribute.name()
                 && name.value_token().ok()?.text_trimmed() == name_to_lookup
             {
-                return Some(attribute);
+                Some(attribute)
+            } else {
+                None
             }
-            None
         })
     }
 }

Or more idiomatically:

 impl HtmlAttributeList {
     pub fn find_by_name(&self, name_to_lookup: &str) -> Option<HtmlAttribute> {
         self.iter().find_map(|attribute| {
-            if let AnyHtmlAttribute::HtmlAttribute(attribute) = attribute
+            let AnyHtmlAttribute::HtmlAttribute(attribute) = attribute else {
+                return None;
+            };
+            if let Ok(name) = attribute.name()
                 && let Ok(name) = attribute.name()
                 && name.value_token().ok()?.text_trimmed() == name_to_lookup
             {
-                return Some(attribute);
+                Some(attribute)
+            } else {
+                None
             }
-            None
         })
     }
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bc4f99 and 223f06c.

⛔ Files ignored due to path filters (3)
  • crates/biome_html_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/valid.html.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (6)
  • crates/biome_html_analyze/src/lint/nursery/no_sync_scripts.rs (1 hunks)
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html (1 hunks)
  • crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/valid.html (1 hunks)
  • crates/biome_html_syntax/src/attr_ext.rs (2 hunks)
  • crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (1 hunks)
  • justfile (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
crates/biome_html_syntax/src/attr_ext.rs (2)
crates/biome_html_syntax/src/lib.rs (1)
  • inner_string_text (119-128)
crates/biome_html_syntax/src/element_ext.rs (1)
  • name (55-70)
crates/biome_html_analyze/src/lint/nursery/no_sync_scripts.rs (2)
crates/biome_analyze/src/rule.rs (1)
  • same (246-251)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (2)
  • run (89-108)
  • diagnostic (110-124)
crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (2)
crates/biome_analyze/src/rule.rs (4)
  • recommended (602-605)
  • sources (617-620)
  • same (246-251)
  • domains (632-635)
crates/biome_html_analyze/src/lint/nursery/no_sync_scripts.rs (2)
  • run (44-62)
  • diagnostic (64-78)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (14)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: End-to-end tests
  • GitHub Check: Check Dependencies
  • GitHub Check: Documentation
  • GitHub Check: Test Node.js API
  • GitHub Check: Check JS Files
  • GitHub Check: autofix
  • GitHub Check: Bench (biome_js_analyze)
  • GitHub Check: Bench (biome_js_formatter)
  • GitHub Check: Bench (biome_js_parser)
  • GitHub Check: Bench (biome_configuration)
🔇 Additional comments (6)
justfile (1)

147-147: LGTM!

Properly extends test coverage for the HTML analyzer, consistent with the existing pattern.

Also applies to: 152-152

crates/biome_js_analyze/src/lint/nursery/no_sync_scripts.rs (1)

55-59: LGTM!

Correctly covers both JSX opening and self-closing script elements.

crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/invalid.html (1)

1-1: LGTM!

Appropriate minimal test case for the diagnostic.

crates/biome_html_analyze/tests/specs/nursery/noSyncScripts/valid.html (1)

1-8: LGTM!

Comprehensive coverage of valid cases: non-script elements, inline scripts, and properly async/deferred external scripts.

crates/biome_html_analyze/src/lint/nursery/no_sync_scripts.rs (2)

44-62: LGTM!

Clean validation logic: checks for script element, requires src, and ensures async/defer are absent.


64-78: LGTM!

Clear diagnostic with actionable guidance. Correctly omits Next.js-specific advice for the HTML context.

Comment on lines +89 to +108
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let binding = ctx.query();

match binding {
NoSyncScriptsQuery::JsxOpeningElement(node) => {
let name = node.name().ok()?;
validate_name(&name)?;

let attributes = node.attributes();
validate_attributes(&attributes)
}
NoSyncScriptsQuery::JsxSelfClosingElement(node) => {
let name = node.name().ok()?;
validate_name(&name)?;

let attributes = node.attributes();
validate_attributes(&attributes)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Eliminate duplication in match branches.

Both match arms perform identical operations. Extract common logic.

Apply this diff:

 fn run(ctx: &RuleContext<Self>) -> Self::Signals {
     let binding = ctx.query();
-
-    match binding {
-        NoSyncScriptsQuery::JsxOpeningElement(node) => {
-            let name = node.name().ok()?;
-            validate_name(&name)?;
-
-            let attributes = node.attributes();
-            validate_attributes(&attributes)
-        }
-        NoSyncScriptsQuery::JsxSelfClosingElement(node) => {
-            let name = node.name().ok()?;
-            validate_name(&name)?;
-
-            let attributes = node.attributes();
-            validate_attributes(&attributes)
-        }
-    }
+    let name = binding.name().ok()?;
+    validate_name(&name)?;
+    
+    let attributes = binding.attributes();
+    validate_attributes(&attributes)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let binding = ctx.query();
match binding {
NoSyncScriptsQuery::JsxOpeningElement(node) => {
let name = node.name().ok()?;
validate_name(&name)?;
let attributes = node.attributes();
validate_attributes(&attributes)
}
NoSyncScriptsQuery::JsxSelfClosingElement(node) => {
let name = node.name().ok()?;
validate_name(&name)?;
let attributes = node.attributes();
validate_attributes(&attributes)
}
}
}
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let binding = ctx.query();
match binding {
NoSyncScriptsQuery::JsxOpeningElement(node) | NoSyncScriptsQuery::JsxSelfClosingElement(node) => {
let name = node.name().ok()?;
validate_name(&name)?;
let attributes = node.attributes();
validate_attributes(&attributes)
}
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does not work.

@Netail Netail merged commit 0f0a658 into biomejs:main Nov 15, 2025
20 checks passed
@github-actions github-actions bot mentioned this pull request Nov 14, 2025
@Netail Netail deleted the feat/no-sync-scripts branch November 17, 2025 20:39
@coderabbitai coderabbitai bot mentioned this pull request Nov 17, 2025
ematipico pushed a commit to hamirmahal/biome that referenced this pull request Nov 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Parser Area: parser A-Project Area: project L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Port the no-sync-scripts rule from Next.js

2 participants