From 5462fd37c1ba4ea7d1c35cd0ec84acbd00b359b7 Mon Sep 17 00:00:00 2001 From: Rahul Gaba Date: Thu, 13 May 2021 02:57:19 +0530 Subject: [PATCH] Add a lint rule for using next script component when using inline script for Google Analytics. --- errors/next-script-for-ga.md | 70 +++++++++++ .../lib/rules/next-script-for-ga.js | 48 +++++++ .../lib/utils/nodeAttributes.js | 3 +- .../next-script-for-ga.unit.test.js | 118 ++++++++++++++++++ .../image-optimizer/next.config.js | 2 +- 5 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 errors/next-script-for-ga.md create mode 100644 packages/eslint-plugin-next/lib/rules/next-script-for-ga.js create mode 100644 test/eslint-plugin-next/next-script-for-ga.unit.test.js diff --git a/errors/next-script-for-ga.md b/errors/next-script-for-ga.md new file mode 100644 index 000000000000000..6fad6f8531d4f1e --- /dev/null +++ b/errors/next-script-for-ga.md @@ -0,0 +1,70 @@ +# Next Script for Google Analytics + +### Why This Error Occurred + +An inline script was used for Google analytics which might impact your webpage's performance. + +### Possible Ways to Fix It + +#### Script component (experimental) + +Use the Script component with the right loading strategy to defer loading of the script until necessary. + +```jsx +import Script from 'next/experimental-script' + +const Home = () => { + return ( +
+ +
+ ) +} + +export default Home +``` + +If you are using the [alternative async variant](https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag): + +```jsx +import Script from 'next/experimental-script' + +const Home = () => { + return ( +
+ + +
+ ) +} + +export default Home +``` + +Note: This is still an experimental feature and needs to be enabled via the `experimental.scriptLoader` flag in `next.config.js`. + +### Useful Links + +- [Add analytics.js to Your Site](https://developers.google.com/analytics/devguides/collection/analyticsjs) +- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/) diff --git a/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js b/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js new file mode 100644 index 000000000000000..f7e710788fec5f5 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/next-script-for-ga.js @@ -0,0 +1,48 @@ +const NodeAttributes = require('../utils/nodeAttributes.js') + +const GA_URL = 'www.google-analytics.com/analytics.js' +const ERROR_MSG = + 'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.' + +module.exports = function (context) { + return { + JSXOpeningElement(node) { + if (node.name.name !== 'script') { + return + } + if (node.attributes.length === 0) { + return + } + const attributes = new NodeAttributes(node) + + // Check if the Alternative async tag is being used to add GA. https://developers.google.com/analytics/devguides/collection/analyticsjs#alternative_async_tag + if ( + typeof attributes.value('src') === 'string' && + attributes.value('src').includes(GA_URL) + ) { + return context.report({ + node, + message: ERROR_MSG, + }) + } + + // Check if inline script is being used to add GA. https://developers.google.com/analytics/devguides/collection/analyticsjs#the_google_analytics_tag + if (attributes.has('dangerouslySetInnerHTML')) { + const htmlContent = attributes.value('dangerouslySetInnerHTML')[0].value + .quasis[0].value.raw + + if ( + htmlContent && + htmlContent.includes('www.google-analytics.com/analytics.js') + ) { + context.report({ + node, + message: ERROR_MSG, + }) + } + } + }, + } +} + +module.exports.schema = [] diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js index c9eb583275c8d21..76f0c00828378d6 100644 --- a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js +++ b/packages/eslint-plugin-next/lib/utils/nodeAttributes.js @@ -26,7 +26,8 @@ class NodeAttributes { this.attributes[attribute.name.name].value = attribute.value.value } else if (attribute.value.expression) { this.attributes[attribute.name.name].value = - attribute.value.expression.value + attribute.value.expression.value || + attribute.value.expression.properties } } }) diff --git a/test/eslint-plugin-next/next-script-for-ga.unit.test.js b/test/eslint-plugin-next/next-script-for-ga.unit.test.js new file mode 100644 index 000000000000000..1f35b22e40e6b52 --- /dev/null +++ b/test/eslint-plugin-next/next-script-for-ga.unit.test.js @@ -0,0 +1,118 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/next-script-for-ga') + +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('sync-scripts', rule, { + valid: [ + `import Script from 'next/experimental-script' + + export class Blah extends Head { + render() { + return ( +
+

Hello title

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

Hello title

+ +
+ ); + } + }`, + ], + + invalid: [ + { + code: ` + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + errors: [ + { + message: + 'Use the Script component for loading third party scripts. See: https://nextjs.org/docs/messages/next-script-for-ga.', + type: 'JSXOpeningElement', + }, + ], + }, + ], +}) diff --git a/test/integration/image-optimizer/next.config.js b/test/integration/image-optimizer/next.config.js index 6b05babba937329..c4b4297cf5a8ca5 100644 --- a/test/integration/image-optimizer/next.config.js +++ b/test/integration/image-optimizer/next.config.js @@ -1,2 +1,2 @@ // prettier-ignore -module.exports = { /* replaceme */ } +module.exports = {"images":{"loader":"notreal"}}