Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit a3a1ad2

Browse filesBrowse files
taga3sautofix-ci[bot]Netail
authored
feat(biome_js_analyze): port noBeforeInteractiveScriptOutsideDocument from Next.js (#8580)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maikel van Dort <maikel.van.dort@gmail.com>
1 parent 2fce3df commit a3a1ad2
Copy full SHA for a3a1ad2

File tree

Expand file treeCollapse file tree

17 files changed

+520
-143
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

17 files changed

+520
-143
lines changed
Open diff view settings
Collapse file

‎.changeset/good-kiwis-wonder.md‎

Copy file name to clipboard
+5Lines changed: 5 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
4+
Added the nursery rule [`noBeforeInteractiveScriptOutsideDocument`](https://biomejs.dev/linter/rules/no-before-interactive-script-outside-document/) to the Next.js domain.
5+
This rule prevents usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`.
Collapse file

‎crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs‎

Copy file name to clipboardExpand all lines: crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
+12Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

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

Copy file name to clipboardExpand all lines: crates/biome_configuration/src/analyzer/linter/rules.rs
+167-142Lines changed: 167 additions & 142 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎crates/biome_configuration/src/generated/domain_selector.rs‎

Copy file name to clipboardExpand all lines: crates/biome_configuration/src/generated/domain_selector.rs
+1Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎crates/biome_diagnostics_categories/src/categories.rs‎

Copy file name to clipboardExpand all lines: crates/biome_diagnostics_categories/src/categories.rs
+1Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎crates/biome_js_analyze/src/lint/nursery.rs‎

Copy file name to clipboardExpand all lines: crates/biome_js_analyze/src/lint/nursery.rs
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use biome_analyze::declare_lint_group;
66
pub mod no_ambiguous_anchor_text;
7+
pub mod no_before_interactive_script_outside_document;
78
pub mod no_continue;
89
pub mod no_deprecated_imports;
910
pub mod no_duplicated_spread_props;
@@ -55,4 +56,4 @@ pub mod use_spread;
5556
pub mod use_vue_consistent_define_props_declaration;
5657
pub mod use_vue_define_macros_order;
5758
pub mod use_vue_multi_word_component_names;
58-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , 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_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_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 ,] } }
59+
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_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , 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_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_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 ,] } }
Collapse file
+139Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_diagnostics::Severity;
6+
use biome_js_syntax::jsx_ext::AnyJsxElement;
7+
use biome_rowan::{AstNode, TextRange};
8+
use biome_rule_options::no_before_interactive_script_outside_document::NoBeforeInteractiveScriptOutsideDocumentOptions;
9+
10+
use crate::{
11+
nextjs::{NextUtility, is_next_import},
12+
services::semantic::Semantic,
13+
};
14+
15+
declare_lint_rule! {
16+
/// Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js` in a Next.js project.
17+
///
18+
/// Next.js provides a `<Script>` component from `next/script` to optimize the loading of third-party scripts. Using the `beforeInteractive`
19+
/// strategy allows scripts to be preloaded before any first-party code. `beforeInteractive` scripts must be placed in `pages/_document.js`.
20+
///
21+
/// This rule checks for any usage of the `beforeInteractive` scripts outside of these files.
22+
///
23+
/// ## Examples
24+
///
25+
/// ### Invalid
26+
///
27+
/// ```jsx,expect_diagnostic
28+
/// // pages/index.jsx
29+
/// import Script from 'next/script'
30+
///
31+
/// export default function Index() {
32+
/// return (
33+
/// <div>
34+
/// <Script
35+
/// src="https://example.com/script.js"
36+
/// strategy="beforeInteractive"
37+
/// ></Script>
38+
/// </div>
39+
/// )
40+
/// }
41+
/// ```
42+
///
43+
/// ### Valid
44+
///
45+
/// ```jsx,ignore
46+
/// // pages/_document.jsx
47+
/// import { Html, Head, Main, NextScript } from 'next/document'
48+
/// import Script from 'next/script'
49+
///
50+
/// export default function Document() {
51+
/// return (
52+
/// <Html>
53+
/// <Head />
54+
/// <body>
55+
/// <Main />
56+
/// <NextScript />
57+
/// <Script
58+
/// src="https://example.com/script.js"
59+
/// strategy="beforeInteractive"
60+
/// ></Script>
61+
/// </body>
62+
/// </Html>
63+
/// )
64+
/// }
65+
/// ```
66+
///
67+
pub NoBeforeInteractiveScriptOutsideDocument {
68+
version: "next",
69+
name: "noBeforeInteractiveScriptOutsideDocument",
70+
language: "jsx",
71+
sources: &[RuleSource::EslintNext("no-before-interactive-script-outside-document").same()],
72+
recommended: false,
73+
severity: Severity::Warning,
74+
domains: &[RuleDomain::Next],
75+
}
76+
}
77+
78+
impl Rule for NoBeforeInteractiveScriptOutsideDocument {
79+
type Query = Semantic<AnyJsxElement>;
80+
type State = TextRange;
81+
type Signals = Option<Self::State>;
82+
type Options = NoBeforeInteractiveScriptOutsideDocumentOptions;
83+
84+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
85+
let is_in_app_dir = ctx
86+
.file_path()
87+
.ancestors()
88+
.any(|a| a.file_name().is_some_and(|f| f == "app" && a.is_dir()));
89+
// should not run in app dir
90+
if is_in_app_dir {
91+
return None;
92+
}
93+
94+
let jsx_element = ctx.query();
95+
let element_name = jsx_element.name().ok()?.name_value_token().ok()?;
96+
if element_name.text_trimmed() != "Script" {
97+
return None;
98+
}
99+
100+
let semantic_model = ctx.model();
101+
let reference = jsx_element.name().ok()?;
102+
let reference = reference.as_jsx_reference_identifier()?;
103+
let binding = semantic_model.binding(reference)?;
104+
if !is_next_import(&binding, NextUtility::Script) {
105+
return None;
106+
}
107+
108+
let strategy_attribute = jsx_element.find_attribute_by_name("strategy")?;
109+
let strategy_attribute_value = strategy_attribute.as_static_value()?;
110+
let strategy_attribute_value = strategy_attribute_value.text();
111+
if strategy_attribute_value != "beforeInteractive" {
112+
return None;
113+
}
114+
115+
let path = ctx.file_path();
116+
117+
let file_name = path.file_stem()?;
118+
119+
// pages/_document.(js|ts|jsx|tsx)
120+
let is_in_pages_dir = path.parent()?.file_name().is_some_and(|f| f == "pages");
121+
if is_in_pages_dir && file_name == "_document" {
122+
return None;
123+
}
124+
125+
Some(jsx_element.syntax().text_range_with_trivia())
126+
}
127+
128+
fn diagnostic(_: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
129+
Some(RuleDiagnostic::new(
130+
rule_category!(),
131+
range,
132+
markup! {
133+
"Don't use "<Emphasis>"next/script"</Emphasis>" component with the `"<Emphasis>"beforeInteractive"</Emphasis>"` strategy outside of "<Emphasis>"pages/_document.js"</Emphasis>"."
134+
},
135+
).note(markup! {
136+
"See the "<Hyperlink href="https://nextjs.org/docs/messages/no-before-interactive-script-outside-document">"Next.js docs"</Hyperlink>" for more details."
137+
}))
138+
}
139+
}
Collapse file
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* should not generate diagnostics */
2+
import Script from 'next/script'
3+
4+
export default function RootLayout({ children }) {
5+
return (
6+
<html lang="en">
7+
<body>{children}</body>
8+
<Script
9+
src="https://example.com/script.js"
10+
strategy="beforeInteractive"
11+
/>
12+
</html>
13+
)
14+
}
Collapse file
+22Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: layout.jsx
4+
---
5+
# Input
6+
```jsx
7+
/* should not generate diagnostics */
8+
import Script from 'next/script'
9+
10+
export default function RootLayout({ children }) {
11+
return (
12+
<html lang="en">
13+
<body>{children}</body>
14+
<Script
15+
src="https://example.com/script.js"
16+
strategy="beforeInteractive"
17+
/>
18+
</html>
19+
)
20+
}
21+
22+
```
Collapse file
+19Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* should not generate diagnostics */
2+
import { Html, Head, Main, NextScript } from 'next/document'
3+
import Script from 'next/script'
4+
5+
export default function Document() {
6+
return (
7+
<Html>
8+
<Head />
9+
<body>
10+
<Main />
11+
<NextScript />
12+
<Script
13+
src="https://example.com/script.js"
14+
strategy="beforeInteractive"
15+
></Script>
16+
</body>
17+
</Html>
18+
)
19+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.