123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- // @ts-check
- const path = require('path')
- const fs = require('fs')
- const getRootDir = require('../utils/get-root-dirs')
- 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 = {}
- const url = 'https://nextjs.org/docs/messages/no-html-link-for-pages'
- module.exports = {
- meta: {
- docs: {
- description:
- 'Prevent usage of `<a>` elements to navigate to internal Next.js pages.',
- category: 'HTML',
- recommended: true,
- url,
- },
- type: 'problem',
- schema: [
- {
- oneOf: [
- {
- type: 'string',
- },
- {
- type: 'array',
- uniqueItems: true,
- items: {
- type: 'string',
- },
- },
- ],
- },
- ],
- },
- /**
- * Creates an ESLint rule listener.
- *
- * @param {import('eslint').Rule.RuleContext} context - ESLint rule context
- * @returns {import('eslint').Rule.RuleListener} An ESLint rule listener
- */
- create: function (context) {
- /** @type {(string|string[])[]} */
- const ruleOptions = context.options
- const [customPagesDirectory] = ruleOptions
- const rootDirs = getRootDir(context)
- const pagesDirs = (
- customPagesDirectory
- ? [customPagesDirectory]
- : rootDirs.map((dir) => [
- path.join(dir, 'pages'),
- path.join(dir, 'src', 'pages'),
- ])
- ).flat()
- 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 pageUrls = getUrlFromPagesDirectories('/', foundPagesDirs)
- return {
- JSXOpeningElement(node) {
- if (node.name.name !== 'a') {
- return
- }
- if (node.attributes.length === 0) {
- return
- }
- const target = node.attributes.find(
- (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'target'
- )
- if (target && target.value.value === '_blank') {
- return
- }
- const href = node.attributes.find(
- (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'href'
- )
- if (!href || (href.value && href.value.type !== 'Literal')) {
- return
- }
- const hasDownloadAttr = node.attributes.find(
- (attr) =>
- attr.type === 'JSXAttribute' && attr.name.name === 'download'
- )
- if (hasDownloadAttr) {
- return
- }
- const hrefPath = normalizeURL(href.value.value)
- // Outgoing links are ignored
- if (/^(https?:\/\/|\/\/)/.test(hrefPath)) {
- return
- }
- pageUrls.forEach((pageUrl) => {
- if (pageUrl.test(normalizeURL(hrefPath))) {
- context.report({
- node,
- message: `Do not use an \`<a>\` element to navigate to \`${hrefPath}\`. Use \`<Link />\` from \`next/link\` instead. See: ${url}`,
- })
- }
- })
- },
- }
- },
- }
|