rsnext/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js
JacobLey 527cb97b56
Support multiple pages directories for linting (#25565)
Monorepos may contain multiple NextJS apps, but linting occurs at top-level so all directories must be declared.

Declaring multiple directories via an Array allows loading all to generate a full list of potential URLs.

Updated schema and tests. Also optimized some of the `fs.*Sync` requests that can add up to lots of blocking lookups.

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.

Closes: https://github.com/vercel/next.js/pull/27223
2021-07-20 21:29:54 +00:00

101 lines
2.6 KiB
JavaScript

const path = require('path')
const fs = require('fs')
const {
getUrlFromPagesDirectories,
normalizeURL,
execOnce,
} = require('../utils/url')
const pagesDirWarning = execOnce((pagesDirs) => {
console.warn(
`Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` +
`If using a custom path, please configure with the no-html-link-for-pages rule in your eslint config file`
)
})
// Cache for fs.existsSync lookup.
// Prevent multiple blocking IO requests that have already been calculated.
const fsExistsSyncCache = {}
module.exports = {
meta: {
docs: {
description: 'Prohibit full page refresh for nextjs pages',
category: 'HTML',
recommended: true,
},
fixable: null, // or "code" or "whitespace"
schema: [
{
oneOf: [
{
type: 'string',
},
{
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
},
],
},
],
},
create: function (context) {
const [customPagesDirectory] = context.options
const pagesDirs = customPagesDirectory
? [customPagesDirectory].flat()
: [
path.join(context.getCwd(), 'pages'),
path.join(context.getCwd(), 'src', 'pages'),
]
const foundPagesDirs = pagesDirs.filter((dir) => {
if (fsExistsSyncCache[dir] === undefined) {
fsExistsSyncCache[dir] = fs.existsSync(dir)
}
return fsExistsSyncCache[dir]
})
if (foundPagesDirs.length === 0) {
pagesDirWarning(pagesDirs)
return {}
}
const urls = getUrlFromPagesDirectories('/', foundPagesDirs)
return {
JSXOpeningElement(node) {
if (node.name.name !== 'a') {
return
}
if (node.attributes.length === 0) {
return
}
const href = node.attributes.find(
(attr) => attr.type === 'JSXAttribute' && attr.name.name === 'href'
)
if (!href || href.value.type !== 'Literal') {
return
}
const hrefPath = normalizeURL(href.value.value)
// Outgoing links are ignored
if (/^(https?:\/\/|\/\/)/.test(hrefPath)) {
return
}
urls.forEach((url) => {
if (url.test(normalizeURL(hrefPath))) {
context.report({
node,
message: `Do not use the HTML <a> tag to navigate to ${hrefPath}. Use Link from 'next/link' instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages.`,
})
}
})
},
}
},
}