Skip to content

Commit

Permalink
adding no html-link lint rule to eslint-plugin (vercel#12969)
Browse files Browse the repository at this point in the history
* addinng no html-link lint rule

* fixing lint tests

* adding the utils file

* fixing lock file

* prettier fix
  • Loading branch information
prateekbh authored May 19, 2020
1 parent 9b098f5 commit dc826e3
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 2,418 deletions.
57 changes: 57 additions & 0 deletions packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const path = require('path')
const fs = require('fs')
const { getUrlFromPagesDirectory, normalizeURL } = require('../utils/url')

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: 'Prohibit full page refresh for nextjs pages',
category: 'HTML',
recommended: true,
},
fixable: null, // or "code" or "whitespace"
schema: ['pagesDirectory'],
},

create: function (context) {
const [pagesDirectory] = context.options
const pagesDir = pagesDirectory || path.join(context.getCwd(), 'pages')
if (!fs.existsSync(pagesDir)) {
throw new Error(
`Pages directory cannot be found at ${pagesDir}, if using a custom path, please configure with the no-html-link-for-pages rule`
)
}

const urls = getUrlFromPagesDirectory('/', pagesDir)
return {
JSXOpeningElement(node) {
if (node.name.name !== 'a') {
return
}

if (node.attributes.length === 0) {
return
}

const href = node.attributes.find((attr) => attr.name.name === 'href')

if (!href || href.value.type !== 'Literal') {
return
}

const hrefPath = normalizeURL(href.value.value)
urls.forEach((url) => {
if (url.test(normalizeURL(hrefPath))) {
context.report({
node,
message: `You're using <a> tag to navigate to ${hrefPath}. Use Link from 'next/link' to make sure the app behaves like an SPA.`,
})
}
})
},
}
},
}
77 changes: 77 additions & 0 deletions packages/eslint-plugin-next/lib/utils/url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const fs = require('fs')
const path = require('path')

/**
* Checks if the source is a directory.
* @param {string} source
*/
function isDirectory(source) {
return fs.lstatSync(source).isDirectory()
}

/**
* Checks if the source is a directory.
* @param {string} source
*/
function isSymlink(source) {
return fs.lstatSync(source).isSymbolicLink()
}

/**
* Gets the possible URLs from a directory.
* @param {string} urlprefix
* @param {string} directory
*/
function getUrlFromPagesDirectory(urlPrefix, directory) {
return parseUrlForPages(urlPrefix, directory).map(
(url) => new RegExp(normalizeURL(url))
)
}

/**
* Recursively parse directory for page URLs.
* @param {string} urlprefix
* @param {string} directory
*/
function parseUrlForPages(urlprefix, directory) {
const files = fs.readdirSync(directory)
const res = []
files.forEach((fname) => {
if (/(\.(j|t)sx?)$/.test(fname)) {
fname = fname.replace(/\[.*\]/g, '.*')
if (/^index(\.(j|t)sx?)$/.test(fname)) {
res.push(`${urlprefix}${fname.replace(/^index(\.(j|t)sx?)$/, '')}`)
}
res.push(`${urlprefix}${fname.replace(/(\.(j|t)sx?)$/, '')}`)
} else {
const dirPath = path.join(directory, fname)
if (isDirectory(dirPath) && !isSymlink(dirPath)) {
res.push(...parseUrlForPages(urlprefix + fname + '/', dirPath))
}
}
})
return res
}

/**
* Takes a url and does the following things.
* - replace `index.html` with `/`
* - Makes sure all URLs are have a trainiling `/`
* - Removes query string
* @param {string} url
*/
function normalizeURL(url) {
if (!url) {
return
}
url = url.split('?')[0]
url = url.split('#')[0]
url = url = url.replace(/(\/index\.html)$/, '/')
url = url.endsWith('/') ? url : url + '/'
return url
}

module.exports = {
getUrlFromPagesDirectory,
normalizeURL,
}
1 change: 1 addition & 0 deletions test/eslint-plugin-next/custom-pages/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => {}
1 change: 1 addition & 0 deletions test/eslint-plugin-next/custom-pages/list/[id].jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => {}
102 changes: 102 additions & 0 deletions test/eslint-plugin-next/no-html-link-for-pages.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* eslint-env jest */
const rule = require('@next/eslint-plugin-next/lib/rules/no-html-link-for-pages')
const { Linter } = require('eslint')
const assert = require('assert')
const path = require('path')

const linter = new Linter({ cwd: __dirname })
const linterConfig = {
rules: {
'no-html-link-for-pages': [2, path.join(__dirname, 'custom-pages')],
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
jsx: true,
},
},
}

linter.defineRules({
'no-html-link-for-pages': rule,
})

const validCode = `
import Link from 'next/link';
export class Blah extends Head {
render() {
return (
<div>
<Link href='/'>
<a>Homepage</a>
</Link>
<h1>Hello title</h1>
</div>
);
}
}
`

const invalidStaticCode = `
import Link from 'next/link';
export class Blah extends Head {
render() {
return (
<div>
<a href='/'>Homepage</a>
<h1>Hello title</h1>
</div>
);
}
}
`

const invalidDynamicCode = `
import Link from 'next/link';
export class Blah extends Head {
render() {
return (
<div>
<a href='/list/blah'>Homepage</a>
<h1>Hello title</h1>
</div>
);
}
}
`

describe('no-html-link-for-pages', function () {
it('valid', function () {
const report = linter.verify(validCode, linterConfig, {
filename: 'foo.js',
})
assert.deepEqual(report, [])
})

it('invalid static route', function () {
const [report] = linter.verify(invalidStaticCode, linterConfig, {
filename: 'foo.js',
})
assert.notEqual(report, undefined, 'No lint errors found.')
assert.equal(
report.message,
"You're using <a> tag to navigate to /. Use Link from 'next/link' to make sure the app behaves like an SPA."
)
})

it('invalid dynamic route', function () {
const [report] = linter.verify(invalidDynamicCode, linterConfig, {
filename: 'foo.js',
})
assert.notEqual(report, undefined, 'No lint errors found.')
assert.equal(
report.message,
"You're using <a> tag to navigate to /list/blah/. Use Link from 'next/link' to make sure the app behaves like an SPA."
)
})
})
Loading

0 comments on commit dc826e3

Please sign in to comment.