Skip to content

Commit a28e775

Browse files
authored
[ESLint] Disallow <Script /> inside _document.js & <Script /> inside the next/head component (#27257)
## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. [Feature Request](#26365) - [x] Eslint unit ests added - [x] Errors have helpful link attached, see `contributing.md` Let me know if this looks good or something needs to be changed. I still need to add the error links and improve the eslint error messages. I don't know if the CI runs the ESLint tests, but current all pass locally
1 parent eddf205 commit a28e775

File tree

8 files changed

+337
-0
lines changed

8 files changed

+337
-0
lines changed

errors/manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,14 @@
427427
"title": "sharp-missing-in-production",
428428
"path": "/errors/sharp-missing-in-production.md"
429429
},
430+
{
431+
"title": "script-in-document-page",
432+
"path": "/errors/no-script-in-document-page.md"
433+
},
434+
{
435+
"title": "script-in-head-component",
436+
"path": "/errors/no-script-in-head-component.md"
437+
},
430438
{
431439
"title": "max-custom-routes-reached",
432440
"path": "/errors/max-custom-routes-reached.md"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Script component inside \_document.js
2+
3+
#### Why This Error Occurred
4+
5+
You can't use the `next/script` component inside the `_document.js` page. That's because the `_document.js` page only runs on the server and `next/script` has client-side functionality to ensure loading order.
6+
7+
#### Possible Ways to Fix It
8+
9+
If you want a global script, instead use the `_app.js` page.
10+
11+
```jsx
12+
import Script from 'next/script'
13+
14+
function MyApp({ Component, pageProps }) {
15+
return (
16+
<>
17+
<Script src="/my-script.js" />
18+
<Component {...pageProps} />
19+
</>
20+
)
21+
}
22+
23+
export default MyApp
24+
```
25+
26+
- [custom-app](https://nextjs.org/docs/advanced-features/custom-app)
27+
- [next-script](https://nextjs.org/docs/basic-features/script#usage)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Script component inside Head component
2+
3+
#### Why This Error Occurred
4+
5+
The `next/script` component shouldn't be placed inside the `next/head` component
6+
7+
#### Possible Ways to Fix It
8+
9+
Move the `<Script />` component outside of `<Head>...</Head>`
10+
11+
**Before**
12+
13+
```js
14+
import Script from 'next/script'
15+
import Head from 'next/head'
16+
17+
export default function Index() {
18+
return (
19+
<Head>
20+
<title>Next.js</title>
21+
<Script src="/my-script.js" />
22+
</Head>
23+
)
24+
}
25+
```
26+
27+
**After**
28+
29+
```js
30+
import Script from 'next/script'
31+
import Head from 'next/head'
32+
33+
export default function Index() {
34+
return (
35+
<>
36+
<Head>
37+
<title>Next.js</title>
38+
</Head>
39+
<Script src="/my-script.js" />
40+
</>
41+
)
42+
}
43+
```
44+
45+
### Useful links
46+
47+
- [next/head](https://nextjs.org/docs/api-reference/next/head)
48+
- [next/script](https://nextjs.org/docs/basic-features/script#usage)

packages/eslint-plugin-next/lib/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module.exports = {
1212
'link-passhref': require('./rules/link-passhref'),
1313
'no-document-import-in-page': require('./rules/no-document-import-in-page'),
1414
'no-head-import-in-document': require('./rules/no-head-import-in-document'),
15+
'no-script-in-document': require('./rules/no-script-in-document'),
16+
'no-script-in-head': require('./rules/no-script-in-head'),
1517
'no-typos': require('./rules/no-typos'),
1618
'no-duplicate-head': require('./rules/no-duplicate-head'),
1719
},
@@ -31,6 +33,8 @@ module.exports = {
3133
'@next/next/link-passhref': 1,
3234
'@next/next/no-document-import-in-page': 2,
3335
'@next/next/no-head-import-in-document': 2,
36+
'@next/next/no-script-in-document': 2,
37+
'@next/next/no-script-in-head': 2,
3438
'@next/next/no-typos': 1,
3539
'@next/next/no-duplicate-head': 2,
3640
},
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const path = require('path')
2+
3+
module.exports = {
4+
meta: {
5+
docs: {
6+
description: 'Disallow importing next/script inside pages/_document.js',
7+
recommended: true,
8+
},
9+
},
10+
create: function (context) {
11+
return {
12+
ImportDeclaration(node) {
13+
if (node.source.value !== 'next/script') {
14+
return
15+
}
16+
17+
const document = context.getFilename().split('pages')[1]
18+
if (!document || !path.parse(document).name.startsWith('_document')) {
19+
return
20+
}
21+
22+
context.report({
23+
node,
24+
message: `next/script should not be used in pages/_document.js. See: https://nextjs.org/docs/messages/no-script-in-document-page `,
25+
})
26+
},
27+
}
28+
},
29+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module.exports = {
2+
meta: {
3+
docs: {
4+
description: 'Disallow using next/script inside the next/head component',
5+
recommended: true,
6+
},
7+
},
8+
create: function (context) {
9+
let isNextHead = null
10+
11+
return {
12+
ImportDeclaration(node) {
13+
if (node.source.value === 'next/head') {
14+
isNextHead = node.source.value
15+
}
16+
17+
if (node.source.value !== 'next/script') {
18+
return
19+
}
20+
},
21+
JSXElement(node) {
22+
if (!isNextHead) {
23+
return
24+
}
25+
26+
if (
27+
node.openingElement &&
28+
node.openingElement.name &&
29+
node.openingElement.name.name !== 'Head'
30+
) {
31+
return
32+
}
33+
34+
const scriptTag = node.children.find(
35+
(child) =>
36+
child.openingElement &&
37+
child.openingElement.name &&
38+
child.openingElement.name.name === 'Script'
39+
)
40+
41+
if (scriptTag) {
42+
context.report({
43+
node,
44+
message:
45+
"next/script shouldn't be used inside next/head. See: https://nextjs.org/docs/messages/no-script-in-head-component ",
46+
})
47+
}
48+
},
49+
}
50+
},
51+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
const rule = require('@next/eslint-plugin-next/lib/rules/no-script-in-document')
2+
3+
const RuleTester = require('eslint').RuleTester
4+
5+
RuleTester.setDefaultConfig({
6+
parserOptions: {
7+
ecmaVersion: 2018,
8+
sourceType: 'module',
9+
ecmaFeatures: {
10+
modules: true,
11+
jsx: true,
12+
},
13+
},
14+
})
15+
16+
var ruleTester = new RuleTester()
17+
ruleTester.run('no-script-import-in-document', rule, {
18+
valid: [
19+
{
20+
code: `import Document, { Html, Head, Main, NextScript } from 'next/document'
21+
22+
class MyDocument extends Document {
23+
static async getInitialProps(ctx) {
24+
//...
25+
}
26+
27+
render() {
28+
return (
29+
<Html>
30+
<Head/>
31+
</Html>
32+
)
33+
}
34+
}
35+
36+
export default MyDocument
37+
`,
38+
filename: 'pages/_document.js',
39+
},
40+
{
41+
code: `import Document, { Html, Head, Main, NextScript } from 'next/document'
42+
43+
class MyDocument extends Document {
44+
render() {
45+
return (
46+
<Html>
47+
<Head>
48+
<meta charSet="utf-8" />
49+
</Head>
50+
</Html>
51+
)
52+
}
53+
}
54+
55+
export default MyDocument
56+
`,
57+
filename: 'pages/_document.tsx',
58+
},
59+
],
60+
invalid: [
61+
{
62+
code: `
63+
import Document, { Html, Main, NextScript } from 'next/document'
64+
import Script from 'next/script'
65+
66+
class MyDocument extends Document {
67+
render() {
68+
return (
69+
<Html>
70+
<Head />
71+
</Html>
72+
)
73+
}
74+
}
75+
76+
export default MyDocument
77+
`,
78+
filename: 'pages/_document.js',
79+
errors: [
80+
{
81+
message: `next/script should not be used in pages/_document.js. See: https://nextjs.org/docs/messages/no-script-in-document-page `,
82+
},
83+
],
84+
},
85+
{
86+
code: `
87+
import Document, { Html, Main, NextScript } from 'next/document'
88+
import NextScriptTag from 'next/script'
89+
90+
class MyDocument extends Document {
91+
render() {
92+
return (
93+
<Html>
94+
<Head>
95+
<meta charSet="utf-8" />
96+
</Head>
97+
<body>
98+
<Main />
99+
<NextScript />
100+
<NextScriptTag />
101+
</body>
102+
</Html>
103+
)
104+
}
105+
}
106+
107+
export default MyDocument
108+
`,
109+
filename: 'pages/_document.js',
110+
errors: [
111+
{
112+
message: `next/script should not be used in pages/_document.js. See: https://nextjs.org/docs/messages/no-script-in-document-page `,
113+
},
114+
],
115+
},
116+
],
117+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const rule = require('@next/eslint-plugin-next/lib/rules/no-script-in-head.js')
2+
const RuleTester = require('eslint').RuleTester
3+
4+
RuleTester.setDefaultConfig({
5+
parserOptions: {
6+
ecmaVersion: 2018,
7+
sourceType: 'module',
8+
ecmaFeatures: {
9+
modules: true,
10+
jsx: true,
11+
},
12+
},
13+
})
14+
15+
var ruleTester = new RuleTester()
16+
ruleTester.run('no-script-in-head', rule, {
17+
valid: [
18+
`import Script from "next/script";
19+
const Head = ({children}) => children
20+
21+
export default function Index() {
22+
return (
23+
<Head>
24+
<Script></Script>
25+
</Head>
26+
);
27+
}
28+
`,
29+
],
30+
31+
invalid: [
32+
{
33+
code: `
34+
import Head from "next/head";
35+
import Script from "next/script";
36+
37+
export default function Index() {
38+
return (
39+
<Head>
40+
<Script></Script>
41+
</Head>
42+
);
43+
}`,
44+
filename: 'pages/index.js',
45+
errors: [
46+
{
47+
message:
48+
"next/script shouldn't be used inside next/head. See: https://nextjs.org/docs/messages/no-script-in-head-component ",
49+
},
50+
],
51+
},
52+
],
53+
})

0 commit comments

Comments
 (0)