Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add eslint rule for id attribute on inline next/script #27853

Merged
merged 7 commits into from Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/basic-features/eslint.md
Expand Up @@ -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) |

- ✔: Enabled in the recommended configuration
Expand Down
3 changes: 2 additions & 1 deletion docs/basic-features/script.md
Expand Up @@ -131,13 +131,14 @@ export default function Home() {
```js
import Script from 'next/script'

<Script strategy="lazyOnload">
<Script id="show-banner" strategy="lazyOnload">
{`document.getElementById('banner').removeClass('hidden')`}
</Script>

// or

<Script
id="show-banner"
dangerouslySetInnerHTML={{
__html: `document.getElementById('banner').removeClass('hidden')`
}}
Expand Down
26 changes: 26 additions & 0 deletions errors/inline-script-id.md
@@ -0,0 +1,26 @@
# next/script components with inline content require an `id` attribute

## Why This Error Occurred

`next/script` components with inline content require an `id` attribute for deduplication.
stefanprobst marked this conversation as resolved.
Show resolved Hide resolved

## Possible Ways to Fix It

Add an `id` attribute to the `next/script` component.

```jsx
import Script from 'next/script'

export default function App({ Component, pageProps }) {
return (
<>
<Script id="my-script">{`console.log('Hello world!');`}</Script>
<Component {...pageProps} />
</>
)
}
```

## Useful links

- [Docs for Next.js Script component](https://nextjs.org/docs/basic-features/script)
4 changes: 4 additions & 0 deletions errors/manifest.json
Expand Up @@ -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" },
{
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Expand Up @@ -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: {
Expand All @@ -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': {
Expand Down
42 changes: 42 additions & 0 deletions 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',
})
}
}
},
}
},
}
156 changes: 156 additions & 0 deletions 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 (
<Script id="test-script">
{\`console.log('Hello world');\`}
</Script>
)
}`,
},
{
code: `import Script from 'next/script';

export default function TestPage() {
return (
<Script
id="test-script"
dangerouslySetInnerHTML={{
__html: \`console.log('Hello world');\`
}}
/>
)
}`,
},
{
code: `import Script from 'next/script';

export default function TestPage() {
return (
<Script src="https://example.com" />
)
}`,
},
{
code: `import MyScript from 'next/script';

export default function TestPage() {
return (
<MyScript id="test-script">
{\`console.log('Hello world');\`}
</MyScript>
)
}`,
},
{
code: `import MyScript from 'next/script';

export default function TestPage() {
return (
<MyScript
id="test-script"
dangerouslySetInnerHTML={{
__html: \`console.log('Hello world');\`
}}
/>
)
}`,
},
],
invalid: [
{
code: `import Script from 'next/script';

export default function TestPage() {
return (
<Script>
{\`console.log('Hello world');\`}
</Script>
)
}`,
errors: [
{
message: errorMessage,
type: 'JSXElement',
},
],
},
{
code: `import Script from 'next/script';

export default function TestPage() {
return (
<Script
dangerouslySetInnerHTML={{
__html: \`console.log('Hello world');\`
}}
/>
)
}`,
errors: [
{
message: errorMessage,
type: 'JSXElement',
},
],
},
{
code: `import MyScript from 'next/script';

export default function TestPage() {
return (
<MyScript>
{\`console.log('Hello world');\`}
</MyScript>
)
}`,
errors: [
{
message: errorMessage,
type: 'JSXElement',
},
],
},
{
code: `import MyScript from 'next/script';

export default function TestPage() {
return (
<MyScript
dangerouslySetInnerHTML={{
__html: \`console.log('Hello world');\`
}}
/>
)
}`,
errors: [
{
message: errorMessage,
type: 'JSXElement',
},
],
},
],
})
2 changes: 1 addition & 1 deletion test/integration/script-loader/pages/page3.js
Expand Up @@ -3,7 +3,7 @@ import Script from 'next/script'
const Page = () => {
return (
<div class="container">
<Script>
<Script id="inline-script">
{`(window.onload = function () {
const newDiv = document.createElement('div')
newDiv.id = 'onload-div'
Expand Down