-
-
Notifications
You must be signed in to change notification settings - Fork 501
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linter): implement @next/next/no-unwanted-polyfillio (#2197)
implement @next/next/no-unwanted-polyfillio Related issue: #1929 original implementation - code: https://github.com/vercel/next.js/blob/canary/packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts - test: https://github.com/vercel/next.js/blob/canary/test/unit/eslint-plugin-next/no-unwanted-polyfillio.test.ts - doc: https://nextjs.org/docs/messages/no-unwanted-polyfillio
- Loading branch information
Showing
3 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
275 changes: 275 additions & 0 deletions
275
crates/oxc_linter/src/rules/nextjs/no_unwanted_polyfillio.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
use oxc_ast::{ | ||
ast::{JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElementName, JSXIdentifier}, | ||
AstKind, | ||
}; | ||
use oxc_diagnostics::{ | ||
miette::{self, Diagnostic}, | ||
thiserror::{self, Error}, | ||
}; | ||
use oxc_macros::declare_oxc_lint; | ||
use oxc_semantic::AstNode; | ||
use oxc_span::Span; | ||
use phf::{phf_set, Set}; | ||
|
||
use crate::{context::LintContext, rule::Rule}; | ||
|
||
#[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.")] | ||
#[diagnostic( | ||
severity(warning), | ||
help("See https://nextjs.org/docs/messages/no-unwanted-polyfillio") | ||
)] | ||
struct NoUnwantedPolyfillioDiagnostic(String, #[label] pub Span); | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct NoUnwantedPolyfillio; | ||
|
||
declare_oxc_lint!( | ||
/// ### What it does | ||
/// Prevent duplicate polyfills from Polyfill.io. | ||
/// | ||
/// ### Why is this bad? | ||
/// You are using polyfills from Polyfill.io and including polyfills already shipped with Next.js. This unnecessarily increases page weight which can affect loading performance. | ||
/// | ||
/// ### Example | ||
/// ```javascript | ||
/// <script src='https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.copyWithin'></script> | ||
/// | ||
/// <script src='https://polyfill.io/v3/polyfill.min.js?features=WeakSet%2CPromise%2CPromise.prototype.finally%2Ces2015%2Ces5%2Ces6'></script> | ||
/// ``` | ||
NoUnwantedPolyfillio, | ||
correctness | ||
); | ||
|
||
// Keep in sync with next.js polyfills file : https://github.com/vercel/next.js/blob/master/packages/next-polyfill-nomodule/src/index.js | ||
const NEXT_POLYFILLED_FEATURES: Set<&'static str> = phf_set! { | ||
"Array.prototype.@@iterator", | ||
"Array.prototype.at", | ||
"Array.prototype.copyWithin", | ||
"Array.prototype.fill", | ||
"Array.prototype.find", | ||
"Array.prototype.findIndex", | ||
"Array.prototype.flatMap", | ||
"Array.prototype.flat", | ||
"Array.from", | ||
"Array.prototype.includes", | ||
"Array.of", | ||
"Function.prototype.name", | ||
"fetch", | ||
"Map", | ||
"Number.EPSILON", | ||
"Number.Epsilon", | ||
"Number.isFinite", | ||
"Number.isNaN", | ||
"Number.isInteger", | ||
"Number.isSafeInteger", | ||
"Number.MAX_SAFE_INTEGER", | ||
"Number.MIN_SAFE_INTEGER", | ||
"Number.parseFloat", | ||
"Number.parseInt", | ||
"Object.assign", | ||
"Object.entries", | ||
"Object.fromEntries", | ||
"Object.getOwnPropertyDescriptor", | ||
"Object.getOwnPropertyDescriptors", | ||
"Object.is", | ||
"Object.keys", | ||
"Object.values", | ||
"Reflect", | ||
"Set", | ||
"Symbol", | ||
"Symbol.asyncIterator", | ||
"String.prototype.codePointAt", | ||
"String.prototype.endsWith", | ||
"String.fromCodePoint", | ||
"String.prototype.includes", | ||
"String.prototype.@@iterator", | ||
"String.prototype.padEnd", | ||
"String.prototype.padStart", | ||
"String.prototype.repeat", | ||
"String.raw", | ||
"String.prototype.startsWith", | ||
"String.prototype.trimEnd", | ||
"String.prototype.trimStart", | ||
"URL", | ||
"URL.prototype.toJSON", | ||
"URLSearchParams", | ||
"WeakMap", | ||
"WeakSet", | ||
"Promise", | ||
"Promise.prototype.finally", | ||
"es2015", // Should be covered by babel-preset-env instead. | ||
"es2016", // contains polyfilled 'Array.prototype.includes', 'String.prototype.padEnd' and 'String.prototype.padStart' | ||
"es2017", // contains polyfilled 'Object.entries', 'Object.getOwnPropertyDescriptors', 'Object.values', 'String.prototype.padEnd' and 'String.prototype.padStart' | ||
"es2018", // contains polyfilled 'Promise.prototype.finally' and ''Symbol.asyncIterator' | ||
"es2019", // Contains polyfilled 'Object.fromEntries' and polyfilled 'Array.prototype.flat', 'Array.prototype.flatMap', 'String.prototype.trimEnd' and 'String.prototype.trimStart' | ||
"es5", // Should be covered by babel-preset-env instead. | ||
"es6", // Should be covered by babel-preset-env instead. | ||
"es7", // contains polyfilled 'Array.prototype.includes', 'String.prototype.padEnd' and 'String.prototype.padStart' | ||
}; | ||
|
||
impl Rule for NoUnwantedPolyfillio { | ||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { | ||
if let AstKind::JSXOpeningElement(jsx_el) = node.kind() { | ||
let JSXElementName::Identifier(JSXIdentifier { name: tag_name, .. }) = &jsx_el.name | ||
else { | ||
return; | ||
}; | ||
|
||
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 | ||
} | ||
}); | ||
if !matches!(next_script_import_local_name, Some(import) if tag_name.as_str() == import.as_str()) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
if jsx_el.attributes.len() == 0 { | ||
return; | ||
} | ||
|
||
let Some(JSXAttributeItem::Attribute(src)) = 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() == "src" | ||
) | ||
) | ||
}) else { | ||
return; | ||
}; | ||
|
||
if let Some(JSXAttributeValue::StringLiteral(src_value)) = &src.value { | ||
if src_value.value.as_str().starts_with("https://cdn.polyfill.io/v2/") | ||
|| src_value.value.as_str().starts_with("https://polyfill.io/v3/") | ||
{ | ||
let Ok(url) = url::Url::parse(src_value.value.as_str()) else { | ||
return; | ||
}; | ||
|
||
let Some((_, features_value)) = | ||
url.query_pairs().find(|(key, _)| key == "features") | ||
else { | ||
return; | ||
}; | ||
let unwanted_features: Vec<&str> = features_value | ||
.split(',') | ||
.filter(|feature| NEXT_POLYFILLED_FEATURES.contains(feature)) | ||
.collect(); | ||
if !unwanted_features.is_empty() { | ||
ctx.diagnostic(NoUnwantedPolyfillioDiagnostic( | ||
format!( | ||
"{} {}", | ||
unwanted_features.join(", "), | ||
if unwanted_features.len() > 1 { "are" } else { "is" } | ||
), | ||
src.span, | ||
)); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
fn test() { | ||
use crate::tester::Tester; | ||
|
||
let pass = vec![ | ||
r"import {Head} from 'next/document'; | ||
export class Blah extends Head { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Hello title</h1> | ||
<script src='https://polyfill.io/v3/polyfill.min.js?features=AbortController'></script> | ||
</div> | ||
); | ||
} | ||
}", | ||
r"import {Head} from 'next/document'; | ||
export class Blah extends Head { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Hello title</h1> | ||
<script src='https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver'></script> | ||
</div> | ||
); | ||
} | ||
}", | ||
r" | ||
import Script from 'next/script'; | ||
export function MyApp({ Component, pageProps }) { | ||
return ( | ||
<div> | ||
<Component {...pageProps} /> | ||
<Script src='https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver' /> | ||
</div> | ||
); | ||
}", | ||
]; | ||
|
||
let fail = vec![ | ||
r"import {Head} from 'next/document'; | ||
export class Blah extends Head { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Hello title</h1> | ||
<script src='https://polyfill.io/v3/polyfill.min.js?features=WeakSet%2CPromise%2CPromise.prototype.finally%2Ces2015%2Ces5%2Ces6'></script> | ||
</div> | ||
); | ||
} | ||
}", | ||
r" | ||
export class Blah { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Hello title</h1> | ||
<script src='https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.copyWithin'></script> | ||
</div> | ||
); | ||
} | ||
}", | ||
r"import NextScript from 'next/script'; | ||
export function MyApp({ Component, pageProps }) { | ||
return ( | ||
<div> | ||
<Component {...pageProps} /> | ||
<NextScript src='https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.copyWithin' /> | ||
</div> | ||
); | ||
}", | ||
r"import {Head} from 'next/document'; | ||
export class ES2019Features extends Head { | ||
render() { | ||
return ( | ||
<div> | ||
<h1>Hello title</h1> | ||
<script src='https://polyfill.io/v3/polyfill.min.js?features=Object.fromEntries'></script> | ||
</div> | ||
); | ||
} | ||
}", | ||
]; | ||
|
||
Tester::new(NoUnwantedPolyfillio::NAME, pass, fail).test_and_snapshot(); | ||
} |
41 changes: 41 additions & 0 deletions
41
crates/oxc_linter/src/snapshots/no_unwanted_polyfillio.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
--- | ||
source: crates/oxc_linter/src/tester.rs | ||
expression: no_unwanted_polyfillio | ||
--- | ||
⚠ eslint-plugin-next(no-unwanted-polyfillio): No duplicate polyfills from Polyfill.io are allowed. WeakSet, Promise, Promise.prototype.finally, es2015, es5, es6 are already shipped with Next.js. | ||
╭─[no_unwanted_polyfillio.tsx:7:1] | ||
7 │ <h1>Hello title</h1> | ||
8 │ <script src='https://polyfill.io/v3/polyfill.min.js?features=WeakSet%2CPromise%2CPromise.prototype.finally%2Ces2015%2Ces5%2Ces6'></script> | ||
· ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | ||
9 │ </div> | ||
╰──── | ||
help: See https://nextjs.org/docs/messages/no-unwanted-polyfillio | ||
|
||
⚠ eslint-plugin-next(no-unwanted-polyfillio): No duplicate polyfills from Polyfill.io are allowed. Array.prototype.copyWithin is already shipped with Next.js. | ||
╭─[no_unwanted_polyfillio.tsx:6:1] | ||
6 │ <h1>Hello title</h1> | ||
7 │ <script src='https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.copyWithin'></script> | ||
· ──────────────────────────────────────────────────────────────────────────────── | ||
8 │ </div> | ||
╰──── | ||
help: See https://nextjs.org/docs/messages/no-unwanted-polyfillio | ||
|
||
⚠ eslint-plugin-next(no-unwanted-polyfillio): No duplicate polyfills from Polyfill.io are allowed. Array.prototype.copyWithin is already shipped with Next.js. | ||
╭─[no_unwanted_polyfillio.tsx:6:1] | ||
6 │ <Component {...pageProps} /> | ||
7 │ <NextScript src='https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.copyWithin' /> | ||
· ──────────────────────────────────────────────────────────────────────────────── | ||
8 │ </div> | ||
╰──── | ||
help: See https://nextjs.org/docs/messages/no-unwanted-polyfillio | ||
|
||
⚠ eslint-plugin-next(no-unwanted-polyfillio): No duplicate polyfills from Polyfill.io are allowed. Object.fromEntries is already shipped with Next.js. | ||
╭─[no_unwanted_polyfillio.tsx:7:1] | ||
7 │ <h1>Hello title</h1> | ||
8 │ <script src='https://polyfill.io/v3/polyfill.min.js?features=Object.fromEntries'></script> | ||
· ──────────────────────────────────────────────────────────────────────── | ||
9 │ </div> | ||
╰──── | ||
help: See https://nextjs.org/docs/messages/no-unwanted-polyfillio | ||
|
||
|