diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index ace2312e0..0c0422b0b 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -299,6 +299,7 @@ mod nextjs {
pub mod next_script_for_ga;
pub mod no_assign_module_variable;
pub mod no_async_client_component;
+ pub mod no_before_interactive_script_outside_document;
pub mod no_css_tags;
pub mod no_document_import_in_page;
pub mod no_head_element;
@@ -584,4 +585,5 @@ oxc_macros::declare_all_lint_rules! {
nextjs::no_typos,
nextjs::no_document_import_in_page,
nextjs::no_unwanted_polyfillio,
+ nextjs::no_before_interactive_script_outside_document,
}
diff --git a/crates/oxc_linter/src/rules/nextjs/no_before_interactive_script_outside_document.rs b/crates/oxc_linter/src/rules/nextjs/no_before_interactive_script_outside_document.rs
new file mode 100644
index 000000000..04c8c95fb
--- /dev/null
+++ b/crates/oxc_linter/src/rules/nextjs/no_before_interactive_script_outside_document.rs
@@ -0,0 +1,389 @@
+use oxc_ast::{
+ ast::{JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElementName, JSXIdentifier},
+ AstKind,
+};
+use oxc_diagnostics::{
+ miette::{self, Diagnostic},
+ thiserror::Error,
+};
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+use crate::{
+ context::LintContext,
+ rule::Rule,
+ utils::{get_next_script_import_local_name, is_document_page, is_in_app_dir},
+ AstNode,
+};
+
+#[derive(Debug, Error, Diagnostic)]
+#[error("eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`")]
+#[diagnostic(
+ severity(warning),
+ help("See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document")
+)]
+struct NoBeforeInteractiveScriptOutsideDocumentDiagnostic(#[label] pub Span);
+
+#[derive(Debug, Default, Clone)]
+pub struct NoBeforeInteractiveScriptOutsideDocument;
+
+declare_oxc_lint!(
+ /// ### What it does
+ /// Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`.
+ ///
+ /// ### Why is this bad?
+ ///
+ ///
+ /// ### Example
+ /// ```javascript
+ /// ```
+ NoBeforeInteractiveScriptOutsideDocument,
+ correctness
+);
+
+impl Rule for NoBeforeInteractiveScriptOutsideDocument {
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if let AstKind::JSXOpeningElement(jsx_el) = node.kind() {
+ let Some(file_path) = ctx.file_path().to_str() else { return };
+ if is_in_app_dir(file_path) {
+ return;
+ }
+ let JSXElementName::Identifier(JSXIdentifier { name: tag_name, .. }) = &jsx_el.name
+ else {
+ return;
+ };
+ if jsx_el.attributes.len() == 0 {
+ return;
+ }
+
+ let Some(JSXAttributeItem::Attribute(strategy)) =
+ jsx_el.attributes.iter().find(|attr| {
+ matches!(
+ attr,
+ JSXAttributeItem::Attribute(jsx_attr)
+ if matches!(
+ &jsx_attr.name,
+ JSXAttributeName::Identifier(id) if id.name.as_str() == "strategy"
+ )
+ )
+ })
+ else {
+ return;
+ };
+
+ if let Some(JSXAttributeValue::StringLiteral(strategy_value)) = &strategy.value {
+ if strategy_value.value.as_str() == "beforeInteractive" {
+ if is_document_page(file_path) {
+ return;
+ }
+ let next_script_import_local_name = get_next_script_import_local_name(ctx);
+ if !matches!(next_script_import_local_name, Some(import) if tag_name.as_str() == import.as_str())
+ {
+ return;
+ }
+ ctx.diagnostic(NoBeforeInteractiveScriptOutsideDocumentDiagnostic(
+ strategy.span,
+ ));
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+ use std::path::PathBuf;
+
+ let pass = vec![
+ (
+ r#"import Document, { Html, Main, NextScript } from 'next/document'
+ import Script from 'next/script'
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("pages/_document.js")),
+ ),
+ (
+ r#"import Document, { Html, Main, NextScript } from 'next/document'
+ import ScriptComponent from 'next/script'
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("pages/_document.tsx")),
+ ),
+ (
+ r#"import Document, { Html, Main, NextScript } from 'next/document'
+ import ScriptComponent from 'next/script'
+
+ class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+ }
+
+ export default MyDocument
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("pages/_document.tsx")),
+ ),
+ (
+ r#"import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("/Users/user_name/projects/project-name/app/layout.tsx")),
+ ),
+ (
+ r#"import Script from "next/script";
+
+ export default function test() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("C:\\Users\\username\\projects\\project-name\\app\\layout.tsx")),
+ ),
+ (
+ r#"import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("/Users/user_name/projects/project-name/src/app/layout.tsx")),
+ ),
+ (
+ r#"import Script from "next/script";
+
+ export default function test() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from(
+ "C:\\Users\\username\\projects\\project-name\\src\\app\\layout.tsx",
+ )),
+ ),
+ ];
+
+ let fail = vec![
+ (
+ r#"import Head from "next/head";
+ import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("pages/index.js")),
+ ),
+ (
+ r#" import Head from "next/head";
+ import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("components/outside-known-dirs.js")),
+ ),
+ (
+ r#" import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("/Users/user_name/projects/project-name/pages/layout.tsx")),
+ ),
+ (
+ r#" import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("C:\\Users\\username\\projects\\project-name\\pages\\layout.tsx")),
+ ),
+ (
+ r#" import Script from "next/script";
+
+ export default function Index() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from("/Users/user_name/projects/project-name/src/pages/layout.tsx")),
+ ),
+ (
+ r#" import Script from "next/script";
+
+ export default function test() {
+ return (
+
+ {children}
+
+
+ );
+ }
+ "#,
+ None,
+ None,
+ Some(PathBuf::from(
+ "C:\\Users\\username\\projects\\project-name\\src\\pages\\layout.tsx",
+ )),
+ ),
+ ];
+
+ Tester::new(NoBeforeInteractiveScriptOutsideDocument::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/rules/nextjs/no_document_import_in_page.rs b/crates/oxc_linter/src/rules/nextjs/no_document_import_in_page.rs
index 7f6f3c115..eafb97a97 100644
--- a/crates/oxc_linter/src/rules/nextjs/no_document_import_in_page.rs
+++ b/crates/oxc_linter/src/rules/nextjs/no_document_import_in_page.rs
@@ -6,7 +6,7 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
-use crate::{context::LintContext, rule::Rule, AstNode};
+use crate::{context::LintContext, rule::Rule, utils::is_document_page, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-next(no-document-import-in-page): `` from `next/document` should not be imported outside of `pages/_document.js`. See: https://nextjs.org/docs/messages/no-document-import-in-page")]
@@ -46,9 +46,8 @@ impl Rule for NoDocumentImportInPage {
}
let Some(path) = ctx.file_path().to_str() else { return };
- let Some(page) = path.split("pages").last() else { return };
- if page.starts_with("/_document") || page.starts_with("\\_document") {
+ if is_document_page(path) {
return;
}
@@ -64,7 +63,7 @@ fn test() {
let pass = vec![
(
r#"import Document from "next/document"
-
+
export default class MyDocument extends Document {
render() {
return (
@@ -80,7 +79,7 @@ fn test() {
),
(
r#"import Document from "next/document"
-
+
export default class MyDocument extends Document {
render() {
return (
diff --git a/crates/oxc_linter/src/rules/nextjs/no_head_element.rs b/crates/oxc_linter/src/rules/nextjs/no_head_element.rs
index 6811475fc..a75cecc72 100644
--- a/crates/oxc_linter/src/rules/nextjs/no_head_element.rs
+++ b/crates/oxc_linter/src/rules/nextjs/no_head_element.rs
@@ -6,7 +6,7 @@ use oxc_diagnostics::{
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
-use crate::{context::LintContext, rule::Rule, AstNode};
+use crate::{context::LintContext, rule::Rule, utils::is_in_app_dir, AstNode};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-next(no-head-element): Do not use `` element. Use `