-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add eslint plugin no-html-links #8156
Changes from all commits
2c8ff51
0ec09b0
6e80680
617153a
695a49a
7434b77
f985e86
237d99e
37e3e0b
add5058
f7f6828
c3a885b
d50a497
72581f5
41ef7a5
bf39059
741e6ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import rule from '../no-html-links'; | ||
import {RuleTester} from './testUtils'; | ||
|
||
const errorsJSX = [{messageId: 'link'}] as const; | ||
|
||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
}); | ||
|
||
ruleTester.run('prefer-docusaurus-link', rule, { | ||
valid: [ | ||
{ | ||
code: '<Link to="/test">test</Link>', | ||
}, | ||
{ | ||
code: '<Link to="https://twitter.com/docusaurus">Twitter</Link>', | ||
}, | ||
{ | ||
code: '<a href="https://twitter.com/docusaurus">Twitter</a>', | ||
options: [{ignoreFullyResolved: true}], | ||
}, | ||
{ | ||
code: '<a href={`https://twitter.com/docusaurus`}>Twitter</a>', | ||
options: [{ignoreFullyResolved: true}], | ||
}, | ||
{ | ||
code: '<a href="mailto:[email protected]">Contact</a> ', | ||
options: [{ignoreFullyResolved: true}], | ||
}, | ||
{ | ||
code: '<a href="tel:123456789">Call</a>', | ||
options: [{ignoreFullyResolved: true}], | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: '<a href="/test">test</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href="https://twitter.com/docusaurus" target="_blank">test</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href="https://twitter.com/docusaurus" target="_blank" rel="noopener noreferrer">test</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href="mailto:[email protected]">Contact</a> ', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href="tel:123456789">Call</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href={``}>Twitter</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href={`https://www.twitter.com/docusaurus`}>Twitter</a>', | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
code: '<a href="www.twitter.com/docusaurus">Twitter</a>', | ||
options: [{ignoreFullyResolved: true}], | ||
errors: errorsJSX, | ||
}, | ||
{ | ||
// TODO we might want to make this test pass | ||
// Can template literals be statically pre-evaluated? (Babel can do it) | ||
// eslint-disable-next-line no-template-curly-in-string | ||
code: '<a href={`https://twitter.com/${"docu" + "saurus"} ${"rex"}`}>Twitter</a>', | ||
options: [{ignoreFullyResolved: true}], | ||
errors: errorsJSX, | ||
}, | ||
], | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import {createRule} from '../util'; | ||
import type {TSESTree} from '@typescript-eslint/types/dist/ts-estree'; | ||
|
||
const docsUrl = 'https://docusaurus.io/docs/docusaurus-core#link'; | ||
|
||
type Options = [ | ||
{ | ||
ignoreFullyResolved: boolean; | ||
}, | ||
]; | ||
|
||
type MessageIds = 'link'; | ||
slorber marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function isFullyResolvedUrl(urlString: string): boolean { | ||
try { | ||
// href gets coerced to a string when it gets rendered anyway | ||
const url = new URL(String(urlString)); | ||
if (url.protocol) { | ||
return true; | ||
} | ||
} catch (e) {} | ||
return false; | ||
} | ||
|
||
export default createRule<Options, MessageIds>({ | ||
slorber marked this conversation as resolved.
Show resolved
Hide resolved
|
||
name: 'no-html-links', | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'enforce using Docusaurus Link component instead of <a> tag', | ||
recommended: false, | ||
}, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
ignoreFullyResolved: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
], | ||
messages: { | ||
link: `Do not use an \`<a>\` element to navigate. Use the \`<Link />\` component from \`@docusaurus/Link\` instead. See: ${docsUrl}`, | ||
}, | ||
}, | ||
defaultOptions: [ | ||
{ | ||
ignoreFullyResolved: false, | ||
}, | ||
], | ||
|
||
create(context, [options]) { | ||
const {ignoreFullyResolved} = options; | ||
|
||
return { | ||
JSXOpeningElement(node) { | ||
if ((node.name as TSESTree.JSXIdentifier).name !== 'a') { | ||
return; | ||
} | ||
|
||
if (ignoreFullyResolved) { | ||
const hrefAttr = node.attributes.find( | ||
(attr): attr is TSESTree.JSXAttribute => | ||
attr.type === 'JSXAttribute' && attr.name.name === 'href', | ||
); | ||
|
||
if (hrefAttr?.value?.type === 'Literal') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add tests for when this is not a literal? Especially template literals. In the future I definitely want this rule to recognize some common interpolation patterns (e.g. if the template starts with |
||
if (isFullyResolvedUrl(String(hrefAttr.value.value))) { | ||
return; | ||
} | ||
} | ||
if (hrefAttr?.value?.type === 'JSXExpressionContainer') { | ||
const container: TSESTree.JSXExpressionContainer = hrefAttr.value; | ||
const {expression} = container; | ||
if (expression.type === 'TemplateLiteral') { | ||
// Simple static string template literals | ||
if ( | ||
expression.expressions.length === 0 && | ||
expression.quasis.length === 1 && | ||
expression.quasis[0]?.type === 'TemplateElement' && | ||
isFullyResolvedUrl(String(expression.quasis[0].value.raw)) | ||
) { | ||
return; | ||
} | ||
// TODO add more complex TemplateLiteral cases here | ||
} | ||
} | ||
} | ||
|
||
context.report({node, messageId: 'link'}); | ||
}, | ||
}; | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wonder if we could also allow
<a>
for hardcoded hash links?(not all links can be evaluated but hardcoded ones could? Maybe eslint can know it starts with a hash? 🤷♂️)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not undoable. Even for template literals, you just need to check
quasis[0][0] === "#"
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
Lets say that we add this to the plugin, should it still warn (encourage) the user to use
<Link>
instead of the a tag?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we can just ignore hashes. This is a bonus, we can merge this PR without this feature if complicated to implement.