Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
ESLint Plugin: passHref is not assigned (vercel#24670)
Browse files Browse the repository at this point in the history
Adds a lint rule warning to the Next.js ESLint plugin if `passHref=true` is not assigned for `<Link>` wrapping a custom component.

Fixes vercel#23713
  • Loading branch information
housseindjirdeh authored May 10, 2021
1 parent 2e34b22 commit 6d43512
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 0 deletions.
32 changes: 32 additions & 0 deletions errors/link-passhref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Link passHref

### Why This Error Occurred

`passHref` was not used for a `Link` component that wraps a custom component. This is needed in order to pass the `href` to the child `<a>` tag.

### Possible Ways to Fix It

If you're using a custom component that wraps an `<a>` tag, make sure to add `passHref`:

```jsx
import Link from 'next/link'
import styled from 'styled-components'

const StyledLink = styled.a`
color: red;
`

function NavLink({ href, name }) {
return (
<Link href={href} passHref>
<StyledLink>{name}</StyledLink>
</Link>
)
}

export default NavLink
```

### Useful Links

- [next/link - Custom Component](https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag)
4 changes: 4 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@
"title": "invalid-webpack-5-version",
"path": "/errors/invalid-webpack-5-version.md"
},
{
"title": "link-passhref",
"path": "/errors/link-passhref.md"
},
{ "title": "manifest.json", "path": "/errors/manifest.json" },
{
"title": "minification-disabled",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
'no-title-in-document-head': require('./rules/no-title-in-document-head'),
'google-font-display': require('./rules/google-font-display'),
'google-font-preconnect': require('./rules/google-font-preconnect'),
'link-passhref': require('./rules/link-passhref'),
},
configs: {
recommended: {
Expand All @@ -19,6 +20,7 @@ module.exports = {
'@next/next/no-title-in-document-head': 1,
'@next/next/google-font-display': 1,
'@next/next/google-font-preconnect': 1,
'@next/next/link-passhref': 1,
},
},
},
Expand Down
65 changes: 65 additions & 0 deletions packages/eslint-plugin-next/lib/rules/link-passhref.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const NodeAttributes = require('../utils/nodeAttributes.js')

module.exports = {
meta: {
docs: {
description:
'Ensure passHref is assigned if child of Link component is a custom component',
category: 'HTML',
recommended: true,
},
fixable: null,
},

create: function (context) {
let linkImport = null

return {
ImportDeclaration(node) {
if (node.source.value === 'next/link') {
linkImport = node.specifiers[0].local.name
}
},

JSXOpeningElement(node) {
if (node.name.name !== 'Link' || node.name.name !== linkImport) {
return
}

const attributes = new NodeAttributes(node)
const children = node.parent.children

if (
!attributes.hasAny() ||
!attributes.has('href') ||
!children.some((attr) => attr.type === 'JSXElement')
) {
return
}

const hasPassHref =
attributes.has('passHref') &&
(typeof attributes.value('passHref') === 'undefined' ||
attributes.value('passHref') === true)

const hasAnchorChild = children.some(
(attr) =>
attr.type === 'JSXElement' &&
attr.openingElement.name.name === 'a' &&
attr.closingElement.name.name === 'a'
)

if (!hasAnchorChild && !hasPassHref) {
context.report({
node,
message: `passHref ${
attributes.value('passHref') !== true
? 'must be set to true'
: 'is missing'
}. See https://nextjs.org/docs/messages/link-passhref.`,
})
}
},
}
},
}
79 changes: 79 additions & 0 deletions test/eslint-plugin-next/link-passhref.unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const rule = require('@next/eslint-plugin-next/lib/rules/link-passhref')
const RuleTester = require('eslint').RuleTester

RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
jsx: true,
},
},
})

var ruleTester = new RuleTester()
ruleTester.run('link-passhref', rule, {
valid: [
`export const Home = () => (
<Link href="/test"></Link>
)`,

`export const Home = () => (
<Link href="/test">
<a>Test</a>
</Link>
)`,

`export const Home = () => (
<Link href="/test" passHref>
<StyledLink>Test</StyledLink>
</Link>
)`,

`const Link = () => <div>Fake Link</div>
const Home = () => (
<Link href="/test">
<MyButton />
</Link>
)`,
],

invalid: [
{
code: `
import Link from 'next/link'
export const Home = () => (
<Link href="/test">
<StyledLink>Test</StyledLink>
</Link>
)`,
errors: [
{
message:
'passHref is missing. See https://nextjs.org/docs/messages/link-passhref.',
type: 'JSXOpeningElement',
},
],
},
{
code: `
import Link from 'next/link'
export const Home = () => (
<Link href="/test" passHref={false}>
<StyledLink>Test</StyledLink>
</Link>
)`,
errors: [
{
message:
'passHref must be set to true. See https://nextjs.org/docs/messages/link-passhref.',
type: 'JSXOpeningElement',
},
],
},
],
})

0 comments on commit 6d43512

Please sign in to comment.