diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index 910b702c4db0e..876aa2e91630e 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -3,6 +3,7 @@ module.exports = { 'no-css-tags': require('./rules/no-css-tags'), 'no-sync-scripts': require('./rules/no-sync-scripts'), 'no-html-link-for-pages': require('./rules/no-html-link-for-pages'), + 'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'), }, configs: { recommended: { @@ -11,6 +12,7 @@ module.exports = { '@next/next/no-css-tags': 1, '@next/next/no-sync-scripts': 1, '@next/next/no-html-link-for-pages': 1, + '@next/next/no-unwanted-polyfillio': 1, }, }, }, diff --git a/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js b/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js new file mode 100644 index 0000000000000..cfcbdda4d67d7 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/no-unwanted-polyfillio.js @@ -0,0 +1,107 @@ +const NEXT_POLYFILLED_FEATURES = [ + 'Array.prototype.@@iterator', + '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', + 'Object.entries', + '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', + 'String.prototype.trim', + 'URL', + 'URLSearchParams', + 'WeakMap', + 'WeakSet', + 'Promise', + 'Promise.prototype.finally', + 'es2015', // Should be covered by babel-preset-env instead. + 'es2016', // Should be covered by babel-preset-env instead. + 'es2017', // Should be covered by babel-preset-env instead. + 'es2018', // Should be covered by babel-preset-env instead. + 'es2019', // Should be covered by babel-preset-env instead. + 'es5', // Should be covered by babel-preset-env instead. + 'es6', // Should be covered by babel-preset-env instead. + 'es7', // Should be covered by babel-preset-env instead. +] + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +module.exports = { + meta: { + docs: { + description: + 'Prohibit unwanted features to be listed in Polyfill.io tag.', + category: 'HTML', + recommended: true, + }, + fixable: null, // or "code" or "whitespace" + }, + + create: function (context) { + return { + 'JSXOpeningElement[name.name=script][attributes.length>0]'(node) { + const srcNode = node.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'src' + ) + if (!srcNode || srcNode.value.type !== 'Literal') { + return + } + const src = srcNode.value.value + if ( + src.startsWith('https://cdn.polyfill.io/v2/') || + src.startsWith('https://polyfill.io/v3/') + ) { + const featureQueryString = new URL(src).searchParams.get('features') + const featuresRequested = (featureQueryString || '').split(',') + const unwantedFeatures = featuresRequested.filter((feature) => + NEXT_POLYFILLED_FEATURES.includes(feature) + ) + if (unwantedFeatures.length > 0) { + context.report({ + node, + message: `You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove ${unwantedFeatures.join( + ', ' + )} from the features list.`, + }) + } + } + }, + } + }, +} diff --git a/test/eslint-plugin-next/no-unwanted-polyfillio.test.js b/test/eslint-plugin-next/no-unwanted-polyfillio.test.js new file mode 100644 index 0000000000000..1659624bc7052 --- /dev/null +++ b/test/eslint-plugin-next/no-unwanted-polyfillio.test.js @@ -0,0 +1,88 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/no-unwanted-polyfillio') + +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('unwanted-polyfillsio', rule, { + valid: [ + `import {Head} from 'next/document'; + + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + `import {Head} from 'next/document'; + + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + ], + + invalid: [ + { + code: `import {Head} from 'next/document'; + + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + errors: [ + { + message: + "You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove WeakSet, Promise, Promise.prototype.finally, es2015, es5, es6 from the features list.", + type: 'JSXOpeningElement', + }, + ], + }, + { + code: ` + export class Blah { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + errors: [ + { + message: + "You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove Array.prototype.copyWithin from the features list.", + type: 'JSXOpeningElement', + }, + ], + }, + ], +})