diff --git a/docs/basic-features/eslint.md b/docs/basic-features/eslint.md index dc4c587de73c..736bbed3c7b2 100644 --- a/docs/basic-features/eslint.md +++ b/docs/basic-features/eslint.md @@ -94,6 +94,7 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/ | ✔️ | [next/no-sync-scripts](https://nextjs.org/docs/messages/no-sync-scripts) | Forbid synchronous scripts | | ✔️ | [next/no-title-in-document-head](https://nextjs.org/docs/messages/no-title-in-document-head) | Disallow using <title> with Head from next/document | | ✔️ | [next/no-unwanted-polyfillio](https://nextjs.org/docs/messages/no-unwanted-polyfillio) | Prevent duplicate polyfills from Polyfill.io | +| ✔️ | [next/inline-script-id](https://nextjs.org/docs/messages/inline-script-id) | Enforce id attribute on next/script components with inline content | | ✔️ | next/no-typos | Ensure no typos were made declaring [Next.js's data fetching function](https://nextjs.org/docs/basic-features/data-fetching) | | ✔️ | [next/next-script-for-ga](https://nextjs.org/docs/messages/next-script-for-ga) | Use the Script component to defer loading of the script until necessary. | diff --git a/docs/basic-features/script.md b/docs/basic-features/script.md index 1d4a0b302e13..ef688170332e 100644 --- a/docs/basic-features/script.md +++ b/docs/basic-features/script.md @@ -131,13 +131,14 @@ export default function Home() { ```js import Script from 'next/script' - // or + + + ) +} +``` + +## Useful links + +- [Docs for Next.js Script component](https://nextjs.org/docs/basic-features/script) diff --git a/errors/manifest.json b/errors/manifest.json index 5645b9cd13e8..d56d23abdcad 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -145,6 +145,10 @@ "title": "incompatible-href-as", "path": "/errors/incompatible-href-as.md" }, + { + "title": "inline-script-id", + "path": "/errors/inline-script-id.md" + }, { "title": "install-sass", "path": "/errors/install-sass.md" }, { "title": "install-sharp", "path": "/errors/install-sharp.md" }, { diff --git a/errors/next-script-for-ga.md b/errors/next-script-for-ga.md index 15d400c098bd..2f367dff127c 100644 --- a/errors/next-script-for-ga.md +++ b/errors/next-script-for-ga.md @@ -20,8 +20,8 @@ const Home = () => { - + /> ) } diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index ae474796056c..6098fa257017 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -16,6 +16,7 @@ module.exports = { 'no-script-in-head': require('./rules/no-script-in-head'), 'no-typos': require('./rules/no-typos'), 'no-duplicate-head': require('./rules/no-duplicate-head'), + 'inline-script-id': require('./rules/inline-script-id'), 'next-script-for-ga': require('./rules/next-script-for-ga'), }, configs: { @@ -39,6 +40,7 @@ module.exports = { '@next/next/no-script-in-head': 2, '@next/next/no-typos': 1, '@next/next/no-duplicate-head': 2, + '@next/next/inline-script-id': 2, }, }, 'core-web-vitals': { diff --git a/packages/eslint-plugin-next/lib/rules/inline-script-id.js b/packages/eslint-plugin-next/lib/rules/inline-script-id.js new file mode 100644 index 000000000000..70843a2be1fb --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/inline-script-id.js @@ -0,0 +1,48 @@ +module.exports = { + meta: { + docs: { + description: + 'next/script components with inline content must specify an `id` attribute.', + recommended: true, + }, + }, + create: function (context) { + let nextScriptImportName = null + + return { + ImportDeclaration(node) { + if (node.source.value === 'next/script') { + nextScriptImportName = node.specifiers[0].local.name + } + }, + JSXElement(node) { + if (nextScriptImportName == null) return + + if ( + node.openingElement && + node.openingElement.name && + node.openingElement.name.name !== nextScriptImportName + ) { + return + } + + const attributes = node.openingElement.attributes + + if ( + node.children.length > 0 || + attributes.some( + (attribute) => attribute.name.name === 'dangerouslySetInnerHTML' + ) + ) { + if (!attributes.some((attribute) => attribute.name.name === 'id')) { + context.report({ + node, + message: + 'next/script components with inline content must specify an `id` attribute. See: https://nextjs.org/docs/messages/inline-script-id', + }) + } + } + }, + } + }, +} diff --git a/test/eslint-plugin-next/inline-script-id.unit.test.js b/test/eslint-plugin-next/inline-script-id.unit.test.js new file mode 100644 index 000000000000..222b78b71cd1 --- /dev/null +++ b/test/eslint-plugin-next/inline-script-id.unit.test.js @@ -0,0 +1,156 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/inline-script-id') + +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +const errorMessage = + 'next/script components with inline content must specify an `id` attribute. See: https://nextjs.org/docs/messages/inline-script-id' + +const ruleTester = new RuleTester() +ruleTester.run('inline-script-id', rule, { + valid: [ + { + code: `import Script from 'next/script'; + + export default function TestPage() { + return ( + + ) + }`, + }, + { + code: `import Script from 'next/script'; + + export default function TestPage() { + return ( + + ) + }`, + errors: [ + { + message: errorMessage, + type: 'JSXElement', + }, + ], + }, + { + code: `import Script from 'next/script'; + + export default function TestPage() { + return ( + -