mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): eslint-plugin-next/no-page-custom-font (#3185)
This commit is contained in:
parent
4defe37f12
commit
fa0093b222
3 changed files with 356 additions and 0 deletions
|
|
@ -353,6 +353,7 @@ mod nextjs {
|
|||
pub mod no_head_element;
|
||||
pub mod no_head_import_in_document;
|
||||
pub mod no_img_element;
|
||||
pub mod no_page_custom_font;
|
||||
pub mod no_script_component_in_head;
|
||||
pub mod no_styled_jsx_in_document;
|
||||
pub mod no_sync_scripts;
|
||||
|
|
@ -702,6 +703,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
nextjs::no_document_import_in_page,
|
||||
nextjs::no_unwanted_polyfillio,
|
||||
nextjs::no_before_interactive_script_outside_document,
|
||||
nextjs::no_page_custom_font,
|
||||
nextjs::no_styled_jsx_in_document,
|
||||
jsdoc::check_access,
|
||||
jsdoc::check_property_names,
|
||||
|
|
|
|||
318
crates/oxc_linter/src/rules/nextjs/no_page_custom_font.rs
Normal file
318
crates/oxc_linter/src/rules/nextjs/no_page_custom_font.rs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
use oxc_ast::{
|
||||
ast::{Class, Function, JSXAttributeItem, JSXElementName},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
enum NoPageCustomFontDiagnostic {
|
||||
#[error("eslint-plugin-next(no-page-custom-font): Custom fonts not added in `pages/_document.js` will only load for a single page. This is discouraged.")]
|
||||
#[diagnostic(
|
||||
severity(warning),
|
||||
help("See: https://nextjs.org/docs/messages/no-page-custom-font")
|
||||
)]
|
||||
NotAddedInDocument(#[label] Span),
|
||||
#[error("eslint-plugin-next(no-page-custom-font): Using `<link />` outside of `<Head>` will disable automatic font optimization. This is discouraged.")]
|
||||
#[diagnostic(
|
||||
severity(warning),
|
||||
help("See: 'https://nextjs.org/docs/messages/no-page-custom-font")
|
||||
)]
|
||||
LinkOutsideOfHead(#[label] Span),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoPageCustomFont;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
/// Prevent page-only custom fonts.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
/// * The custom font you're adding was added to a page - this only adds the font to the specific page and not the entire application.
|
||||
/// * The custom font you're adding was added to a separate component within pages/_document.js - this disables automatic font optimization.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```javascript
|
||||
/// ```
|
||||
NoPageCustomFont,
|
||||
correctness,
|
||||
);
|
||||
|
||||
impl Rule for NoPageCustomFont {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
let AstKind::JSXOpeningElement(element) = node.kind() else { return };
|
||||
if matches!(&element.name, JSXElementName::Identifier(ident) if ident.name != "link") {
|
||||
return;
|
||||
}
|
||||
let is_custom_font = element.attributes.iter().any(
|
||||
|attr| matches!(&attr, JSXAttributeItem::Attribute(attr) if attr.is_identifier("href") && attr.value.is_some()),
|
||||
);
|
||||
|
||||
if !is_custom_font {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut is_inside_export_default = false;
|
||||
for parent_node in ctx.nodes().iter_parents(node.id()) {
|
||||
// export default function/class
|
||||
let kind = parent_node.kind();
|
||||
if matches!(kind, AstKind::ExportDefaultDeclaration(_)) {
|
||||
is_inside_export_default = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// function variable() {}; export default variable;
|
||||
let id = match kind {
|
||||
AstKind::ArrowFunctionExpression(_) => None,
|
||||
AstKind::Function(Function { id, .. }) | AstKind::Class(Class { id, .. }) => {
|
||||
id.clone()
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let name = id.map_or_else(
|
||||
|| {
|
||||
let parent_parent_kind = ctx.nodes().parent_kind(parent_node.id())?;
|
||||
|
||||
let AstKind::VariableDeclarator(declarator) = parent_parent_kind else {
|
||||
return None;
|
||||
};
|
||||
declarator.id.get_identifier().map(ToString::to_string)
|
||||
},
|
||||
|id| Some(id.name.to_string()),
|
||||
);
|
||||
let Some(name) = name else {
|
||||
continue;
|
||||
};
|
||||
if let Some(symbol_id) = ctx.scopes().get_root_binding(&name) {
|
||||
if ctx.symbols().get_flag(symbol_id).is_export() {
|
||||
let is_export_default =
|
||||
ctx.symbols().get_resolved_references(symbol_id).any(|reference| {
|
||||
reference.is_read()
|
||||
&& matches!(
|
||||
ctx.nodes().parent_kind(reference.node_id()),
|
||||
Some(AstKind::ExportDefaultDeclaration(_))
|
||||
)
|
||||
});
|
||||
|
||||
if is_export_default {
|
||||
is_inside_export_default = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let in_document = ctx.file_path().file_name().map_or(false, |file_name| {
|
||||
file_name.to_str().map_or(false, |file_name| file_name.starts_with("_document."))
|
||||
});
|
||||
let span = ctx.nodes().parent_kind(node.id()).unwrap().span();
|
||||
let diagnostic = if in_document {
|
||||
if is_inside_export_default {
|
||||
return;
|
||||
}
|
||||
NoPageCustomFontDiagnostic::LinkOutsideOfHead(span)
|
||||
} else {
|
||||
NoPageCustomFontDiagnostic::NotAddedInDocument(span)
|
||||
};
|
||||
ctx.diagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let filename = Some(PathBuf::from("pages/_document.jsx"));
|
||||
let pass = vec![
|
||||
(
|
||||
r#"import Document, { Html, Head } from "next/document";
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default MyDocument;"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
(
|
||||
r#"import NextDocument, { Html, Head } from "next/document";
|
||||
class Document extends NextDocument {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Document;
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
(
|
||||
r#"export default function CustomDocument() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
)
|
||||
}"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
(
|
||||
r#"function CustomDocument() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomDocument;
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
import Document, { Html, Head } from "next/document";
|
||||
class MyDocument {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
(
|
||||
r#"export default function() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Krona+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
</Html>
|
||||
)
|
||||
}"#,
|
||||
None,
|
||||
None,
|
||||
filename.clone(),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(
|
||||
r#"
|
||||
import Head from 'next/head'
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
Some(PathBuf::from("pages/index.tsx")),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
import Head from 'next/head'
|
||||
|
||||
|
||||
function Links() {
|
||||
return (
|
||||
<>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Open+Sans"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<Links />
|
||||
</Head>
|
||||
<p>Hello world!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
filename,
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(NoPageCustomFont::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
36
crates/oxc_linter/src/snapshots/no_page_custom_font.snap
Normal file
36
crates/oxc_linter/src/snapshots/no_page_custom_font.snap
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
expression: no_page_custom_font
|
||||
---
|
||||
⚠ eslint-plugin-next(no-page-custom-font): Custom fonts not added in `pages/_document.js` will only load for a single page. This is discouraged.
|
||||
╭─[no_page_custom_font.tsx:7:18]
|
||||
6 │ <Head>
|
||||
7 │ ╭─▶ <link
|
||||
8 │ │ href="https://fonts.googleapis.com/css2?family=Inter"
|
||||
9 │ │ rel="stylesheet"
|
||||
10 │ ╰─▶ />
|
||||
11 │ </Head>
|
||||
╰────
|
||||
help: See: https://nextjs.org/docs/messages/no-page-custom-font
|
||||
|
||||
⚠ eslint-plugin-next(no-page-custom-font): Using `<link />` outside of `<Head>` will disable automatic font optimization. This is discouraged.
|
||||
╭─[no_page_custom_font.tsx:8:16]
|
||||
7 │ <>
|
||||
8 │ ╭─▶ <link
|
||||
9 │ │ href="https://fonts.googleapis.com/css2?family=Inter"
|
||||
10 │ │ rel="stylesheet"
|
||||
11 │ ╰─▶ />
|
||||
12 │ <link
|
||||
╰────
|
||||
help: See: 'https://nextjs.org/docs/messages/no-page-custom-font
|
||||
|
||||
⚠ eslint-plugin-next(no-page-custom-font): Using `<link />` outside of `<Head>` will disable automatic font optimization. This is discouraged.
|
||||
╭─[no_page_custom_font.tsx:12:16]
|
||||
11 │ />
|
||||
12 │ ╭─▶ <link
|
||||
13 │ │ href="https://fonts.googleapis.com/css2?family=Open+Sans"
|
||||
14 │ │ rel="stylesheet"
|
||||
15 │ ╰─▶ />
|
||||
16 │ </>
|
||||
╰────
|
||||
help: See: 'https://nextjs.org/docs/messages/no-page-custom-font
|
||||
Loading…
Reference in a new issue