From af16b3b684e72bc3fb6a1bfccd9f3e6031610740 Mon Sep 17 00:00:00 2001 From: Stefan Probst Date: Sat, 7 Aug 2021 21:18:46 +0200 Subject: [PATCH 1/5] feat: add eslint rule for id prop on inline next/script --- docs/basic-features/script.md | 3 +- errors/inline-script-id.md | 26 +++ errors/manifest.json | 4 + packages/eslint-plugin-next/lib/index.js | 2 + .../lib/rules/inline-script-id.js | 42 +++++ .../inline-script-id.unit.test.js | 156 ++++++++++++++++++ test/integration/script-loader/pages/page3.js | 2 +- 7 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 errors/inline-script-id.md create mode 100644 packages/eslint-plugin-next/lib/rules/inline-script-id.js create mode 100644 test/eslint-plugin-next/inline-script-id.unit.test.js 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 09bc27b3a8e7..a65304fa7461 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/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index df01916cca94..6b387969ce59 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -14,6 +14,7 @@ module.exports = { 'no-head-import-in-document': require('./rules/no-head-import-in-document'), 'no-typos': require('./rules/no-typos'), 'no-duplicate-head': require('./rules/no-duplicate-head'), + 'inline-script-id': require('./rules/inline-script-id'), }, configs: { recommended: { @@ -33,6 +34,7 @@ module.exports = { '@next/next/no-head-import-in-document': 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..71db62d76185 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/inline-script-id.js @@ -0,0 +1,42 @@ +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?.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 ( + - + /> ) } 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 index 18b156239c02..eb4c01fd9272 100644 --- a/test/eslint-plugin-next/next-script-for-ga.unit.test.js +++ b/test/eslint-plugin-next/next-script-for-ga.unit.test.js @@ -29,8 +29,8 @@ ruleTester.run('sync-scripts', rule, { -