kaykdm 2024-01-29 22:01:50 +09:00 committed by GitHub
parent f59e87f9c4
commit da3b3057a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 483 additions and 19 deletions

View file

@ -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,
}

View file

@ -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 (
<Html>
<Head>
<meta charSet="utf-8" />
</Head>
<body>
<Main />
<NextScript />
<Script
id="scriptBeforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy="beforeInteractive"
></Script>
</body>
</Html>
)
}
}
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 (
<Html>
<Head>
<meta charSet="utf-8" />
</Head>
<body>
<Main />
<NextScript />
<ScriptComponent
id="scriptBeforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy="beforeInteractive"
></ScriptComponent>
</body>
</Html>
)
}
}
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 (
<Html>
<Head>
<meta charSet="utf-8" />
</Head>
<body>
<Main />
<NextScript />
<ScriptComponent
id="scriptBeforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
></ScriptComponent>
</body>
</Html>
)
}
}
export default MyDocument
"#,
None,
None,
Some(PathBuf::from("pages/_document.tsx")),
),
(
r#"import Script from "next/script";
export default function Index() {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<Script
id="scriptBeforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy="beforeInteractive"
></Script>
);
}
"#,
None,
None,
Some(PathBuf::from("pages/index.js")),
),
(
r#" import Head from "next/head";
import Script from "next/script";
export default function Index() {
return (
<Script
id="scriptBeforeInteractive"
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy="beforeInteractive"
></Script>
);
}
"#,
None,
None,
Some(PathBuf::from("components/outside-known-dirs.js")),
),
(
r#" import Script from "next/script";
export default function Index() {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
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 (
<html lang="en">
<body className={inter.className}>{children}</body>
<Script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
strategy='beforeInteractive'
/>
</html>
);
}
"#,
None,
None,
Some(PathBuf::from(
"C:\\Users\\username\\projects\\project-name\\src\\pages\\layout.tsx",
)),
),
];
Tester::new(NoBeforeInteractiveScriptOutsideDocument::NAME, pass, fail).test_and_snapshot();
}

View file

@ -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): `<Document />` 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 (

View file

@ -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 `<head>` element. Use `<Head />` from `next/head` instead.")]
@ -33,8 +33,7 @@ declare_oxc_lint!(
impl Rule for NoHeadElement {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
let Some(full_file_path) = ctx.file_path().to_str() else { return };
let is_in_app_dir = full_file_path.contains("app/") || full_file_path.contains("app\\");
if is_in_app_dir {
if is_in_app_dir(full_file_path) {
return;
}
if let AstKind::JSXOpeningElement(elem) = node.kind() {
@ -54,7 +53,7 @@ fn test() {
let pass = vec![
(
r"import Head from 'next/head';
export class MyComponent {
render() {
return (

View file

@ -11,7 +11,7 @@ use oxc_semantic::AstNode;
use oxc_span::Span;
use phf::{phf_set, Set};
use crate::{context::LintContext, rule::Rule};
use crate::{context::LintContext, rule::Rule, utils::get_next_script_import_local_name};
#[derive(Debug, Error, Diagnostic)]
#[error("eslint-plugin-next(no-unwanted-polyfillio): No duplicate polyfills from Polyfill.io are allowed. {0} already shipped with Next.js.")]
@ -117,14 +117,7 @@ impl Rule for NoUnwantedPolyfillio {
};
if tag_name.as_str() != "script" {
let next_script_import_local_name =
ctx.semantic().module_record().import_entries.iter().find_map(|entry| {
if entry.module_request.name().as_str() == "next/script" {
Some(entry.local_name.name())
} else {
None
}
});
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;

View file

@ -0,0 +1,59 @@
---
source: crates/oxc_linter/src/tester.rs
expression: no_before_interactive_script_outside_document
---
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy="beforeInteractive"
· ────────────────────────────
10 │ ></Script>
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy="beforeInteractive"
· ────────────────────────────
10 │ ></Script>
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy='beforeInteractive'
· ────────────────────────────
10 │ />
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy='beforeInteractive'
· ────────────────────────────
10 │ />
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy='beforeInteractive'
· ────────────────────────────
10 │ />
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document
⚠ eslint-plugin-next(no-before-interactive-script-outside-document): next/script's `beforeInteractive` strategy should not be used outside of `pages/_document.js`
╭─[no_before_interactive_script_outside_document.tsx:8:1]
8 │ src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js?a=scriptBeforeInteractive"
9 │ strategy='beforeInteractive'
· ────────────────────────────
10 │ />
╰────
help: See https://nextjs.org/docs/messages/no-before-interactive-script-outside-document

View file

@ -1,7 +1,8 @@
mod jest;
mod nextjs;
mod node;
mod react;
mod react_perf;
mod unicorn;
pub use self::{jest::*, node::*, react::*, react_perf::*, unicorn::*};
pub use self::{jest::*, nextjs::*, node::*, react::*, react_perf::*, unicorn::*};

View file

@ -0,0 +1,22 @@
use oxc_span::Atom;
use crate::LintContext;
pub fn is_in_app_dir(file_path: &str) -> bool {
file_path.contains("app/") || file_path.contains("app\\")
}
pub fn is_document_page(file_path: &str) -> bool {
let Some(page) = file_path.split("pages").last() else { return false };
page.starts_with("/_document") || page.starts_with("\\_document")
}
pub fn get_next_script_import_local_name<'a>(ctx: &'a LintContext) -> Option<&'a Atom> {
ctx.semantic().module_record().import_entries.iter().find_map(|entry| {
if entry.module_request.name().as_str() == "next/script" {
Some(entry.local_name.name())
} else {
None
}
})
}