forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
adding no html-link lint rule to eslint-plugin (vercel#12969)
* addinng no html-link lint rule * fixing lint tests * adding the utils file * fixing lock file * prettier fix
- Loading branch information
Showing
6 changed files
with
238 additions
and
2,418 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`, | ||
}) | ||
} | ||
}) | ||
}, | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default () => {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default () => {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
) | ||
}) | ||
}) |
Oops, something went wrong.