diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 827e05964..d9f04c868 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -270,6 +270,7 @@ mod oxc { mod nextjs { pub mod google_font_display; + pub mod google_font_preconnect; } oxc_macros::declare_all_lint_rules! { @@ -509,4 +510,5 @@ oxc_macros::declare_all_lint_rules! { oxc::no_accumulating_spread, oxc::only_used_in_recursion, nextjs::google_font_display, + nextjs::google_font_preconnect, } diff --git a/crates/oxc_linter/src/rules/nextjs/google_font_preconnect.rs b/crates/oxc_linter/src/rules/nextjs/google_font_preconnect.rs new file mode 100644 index 000000000..71083987d --- /dev/null +++ b/crates/oxc_linter/src/rules/nextjs/google_font_preconnect.rs @@ -0,0 +1,109 @@ +use oxc_ast::{ast::JSXElementName, 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_string_literal_prop_value, has_jsx_prop_lowercase}, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error(r#"eslint-plugin-next(google-font-preconnect): `rel="preconnect"` is missing from Google Font."#)] +#[diagnostic( + severity(warning), + help("See: https://nextjs.org/docs/messages/google-font-preconnect") +)] +struct GoogleFontPreconnectDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct GoogleFontPreconnect; + +declare_oxc_lint!( + /// ### What it does + /// + /// + /// ### Why is this bad? + /// + /// + /// ### Example + /// ```javascript + /// ``` + GoogleFontPreconnect, + correctness +); + +impl Rule for GoogleFontPreconnect { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::JSXOpeningElement(jsx_opening_element) = node.kind() else { return }; + + let JSXElementName::Identifier(jsx_opening_element_name) = &jsx_opening_element.name else { + return; + }; + + if jsx_opening_element_name.name.as_str() != "link" { + return; + } + + let Some(href_prop) = has_jsx_prop_lowercase(jsx_opening_element, "href") else { + return; + }; + let Some(href_prop_value) = get_string_literal_prop_value(href_prop) else { return }; + + let preconnect_missing = + has_jsx_prop_lowercase(jsx_opening_element, "rel").map_or(true, |rel_prop| { + let rel_prop_value = get_string_literal_prop_value(rel_prop); + rel_prop_value != Some("preconnect") + }); + + if href_prop_value.starts_with("https://fonts.gstatic.com") && preconnect_missing { + ctx.diagnostic(GoogleFontPreconnectDiagnostic(jsx_opening_element_name.span)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"export const Test = () => ( +
+ + + +
+ ) + "#, + ]; + + let fail = vec![ + r#" + export const Test = () => ( +
+ +
+ ) + "#, + r#" + export const Test = () => ( +
+ +
+ ) + "#, + ]; + + Tester::new_without_config(GoogleFontPreconnect::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/google_font_preconnect.snap b/crates/oxc_linter/src/snapshots/google_font_preconnect.snap new file mode 100644 index 000000000..efdb302bd --- /dev/null +++ b/crates/oxc_linter/src/snapshots/google_font_preconnect.snap @@ -0,0 +1,23 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: google_font_preconnect +--- + ⚠ eslint-plugin-next(google-font-preconnect): `rel="preconnect"` is missing from Google Font. + ╭─[google_font_preconnect.tsx:3:1] + 3 │
+ 4 │ + · ──── + 5 │
+ ╰──── + help: See: https://nextjs.org/docs/messages/google-font-preconnect + + ⚠ eslint-plugin-next(google-font-preconnect): `rel="preconnect"` is missing from Google Font. + ╭─[google_font_preconnect.tsx:3:1] + 3 │
+ 4 │ + · ──── + 5 │
+ ╰──── + help: See: https://nextjs.org/docs/messages/google-font-preconnect + +