Skip to content
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

Add new prefer-ideal-image eslint rule #8826

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Devansu-Yadav
Copy link
Contributor

Pre-flight checklist

  • I have read the Contributing Guidelines on pull requests.
  • If this is a code change: I have written unit tests and/or added dogfooding pages to fully verify the new behavior.
  • If this is a new API or substantial change: the PR has an accompanying issue (closes #0000) and the maintainers have approved on my working plan.

Motivation

Described in #6472. Basically, this plugin enforces the use of @theme/IdealImage component instead of <img> tags.

Test Plan

Added tests using eslint's RuleTester utility.

Test links

Deploy preview: https://deploy-preview-_____--docusaurus-2.netlify.app/

Related issues/PRs

#6472

@Devansu-Yadav
Copy link
Contributor Author

@slorber I still need to fix linting errors and add documentation for this eslint rule, but just wanted to ask, do you think it would be useful to implement auto-fixing capability for this rule?

@netlify
Copy link

netlify bot commented Mar 25, 2023

[V2]

Built without sensitive environment variables

Name Link
🔨 Latest commit 678f850
🔍 Latest deploy log https://app.netlify.com/sites/docusaurus-2/deploys/641f072ae987810008319000
😎 Deploy Preview https://deploy-preview-8826--docusaurus-2.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@github-actions
Copy link

github-actions bot commented Mar 25, 2023

⚡️ Lighthouse report for the deploy preview of this PR

URL Performance Accessibility Best Practices SEO PWA Report
/ 🟠 59 🟢 97 🟢 100 🟢 100 🟠 89 Report
/docs/installation 🟠 80 🟢 100 🟢 100 🟢 100 🟠 89 Report

@@ -16,6 +16,7 @@ export = {
'@docusaurus/string-literal-i18n-messages': 'error',
'@docusaurus/no-html-links': 'warn',
'@docusaurus/prefer-docusaurus-heading': 'warn',
'@docusaurus/prefer-ideal-image': 'warn',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be off by default. Not many use ideal image. It should be off in our own codebase except the website as well.

@Devansu-Yadav
Copy link
Contributor Author

Devansu-Yadav commented Mar 27, 2023

@slorber I still need to fix linting errors and add documentation for this eslint rule, but just wanted to ask, do you think it would be useful to implement auto-fixing capability for this rule?

@Josh-Cena What do you think about this?

@Josh-Cena
Copy link
Collaborator

I don't think we need auto-fix, no. It sounds very non-trivial and has a high risk of breaking otherwise valid code. Auto-fixing should never change code behavior.

@Devansu-Yadav
Copy link
Contributor Author

Devansu-Yadav commented Mar 28, 2023

@slorber @Josh-Cena Seems like some of the checks are failing for this PR because of the following error:

12:50:33 PM: [INFO] [en] Creating an optimized production build...
12:50:50 PM: [info] [webpackbar] Compiling Client
12:50:50 PM: [info] [webpackbar] Compiling Server
12:51:02 PM: [success] [webpackbar] Client: Compiled successfully in 11.76s
12:51:20 PM: [success] [webpackbar] Server: Compiled with some errors in 30.14s
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: TypeError: _chalk.default.bold is not a function
12:51:20 PM: [ERROR] Unable to build website for locale en.
12:51:20 PM: [ERROR] Error: Failed to compile with errors.
12:51:20 PM:     at /opt/build/repo/packages/docusaurus/lib/webpack/utils.js:180:24
12:51:20 PM:     at /opt/build/repo/node_modules/webpack/lib/MultiCompiler.js:554:14
12:51:20 PM:     at processQueueWorker (/opt/build/repo/node_modules/webpack/lib/MultiCompiler.js:491:6)
12:51:20 PM:     at processTicksAndRejections (node:internal/process/task_queues:78:11)

Anything that needs to be done from my side to fix this?

Copy link
Collaborator

@slorber slorber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks but unfortunately that's not so simple. It is preferable that you open an RFC of the behavior you plan to implement before starting the work because the description of the original issue is not really complete and does not mention any design or edge cases to consider.

prefer-ideal-image: ensures @theme/IdealImage is used instead of img tags


We should allow any string link coming from props, dynamic or hardcoded values:

<img src="https://github.com/xyz.png" />

<img src={`${props.githubUrl}.png`} />

<img src="./xyz.png" />

<img src={useBaseUrl("./xyz.png"} />


const imgSrc = "./xyz.png"
<img src={imgSrc} />

Eventually, add an option to reject relative local links?

The only patterns that IMHO we can safely reject are the usage of images that are required/imported locally:

import imgSrc from './path/to/img.png';

<img src={imgSrc} />

<img src={require("./img.png")}/>

The weird chalk build errors you get are a bug that I will fix.

You can get the real errors by turning disableInDev: false, in the plugin options

https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-ideal-image#disableInDev

err TypeError: Cannot read properties of undefined (reading 'src')
    at fallbackParams (main:8332:43)
    at IdealImage.render (main:7750:106)
    at d (main:67040:231)
    at bb (main:67041:16)
    at a.b.render (main:67047:43)
    at a.b.read (main:67046:83)
    at Object.exports.renderToString (main:67057:138)
    at doRender (main:219469:37)
    at async render (main:219387:16)
    at async /Users/sebastienlorber/Desktop/projects/docusaurus/node_modules/p-map/index.js:57:22

This PR is not ready to merge, not because of the CI failures, but because the behavior implemented is not what we need.

],
invalid: [
{
code: "<img src='./path/to/img.png' alt='some alt text' />",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be rejected?

Comment on lines +44 to +45
code: "<img src='./path/to/img.png' srcset='./path/to/img-480w.jpg 480w, ./path/to/img-800w.png 800w' sizes='(max-width: 600px) 480px, 800px' alt='some alt text' />",
errors: errorsJSX,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should not be rejected?

Comment on lines -24 to +43
<img
<Image
className={styles.image}
src={imageURL}
alt={name}
onError={(e) => {
// Image returns 404 if the user's handle changes. We display a
// fallback instead.
e.currentTarget.src =
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none" stroke="%2325c2a0" stroke-width="30" version="1.1"><circle cx="300" cy="230" r="115"/><path stroke-linecap="butt" d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0"/></svg>';
img={{
src: {
src: imageURL,
preSrc:
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none" stroke="%2325c2a0" stroke-width="30" version="1.1"><circle cx="300" cy="230" r="115"/><path stroke-linecap="butt" d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0"/></svg>',
images: [],
},
preSrc:
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none" stroke="%2325c2a0" stroke-width="30" version="1.1"><circle cx="300" cy="230" r="115"/><path stroke-linecap="butt" d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0"/></svg>',
}}
alt={name}
// onError={(e) => {
// // Image returns 404 if the user's handle changes. We display a
// // fallback instead.
// e.currentTarget.src =
// 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600" fill="none" stroke="%2325c2a0" stroke-width="30" version="1.1"><circle cx="300" cy="230" r="115"/><path stroke-linecap="butt" d="M106.81863443903,481.4 a205,205 1 0,1 386.36273112194,0"/></svg>';
// }}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that seems overly complicated, using <img> is fine in this case IMHO

Comment on lines +194 to +198
<Image
className={styles.featureImage}
alt={feature.title}
width={Math.floor(feature.image.width)}
height={Math.floor(feature.image.height)}
src={withBaseUrl(feature.image.src)}
img={withBaseUrl(feature.image.src)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ideal image plugin is not designed to work with SVGs

Eventually we could import the svgs and use them as components instead of using <img src="xyz.svg"/> like we do atm

Copy link
Contributor Author

@Devansu-Yadav Devansu-Yadav Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ideal image plugin is not designed to work with SVGs

I had noticed that it only works with JPEGs and PNGs but somehow completely missed out in this case 😅

Eventually we could import the svgs and use them as components instead of using <img src="xyz.svg"/> like we do atm

@slorber Should I address this change in this PR?

Comment on lines -35 to +37
<img
<Image
alt={translate({message: 'Docusaurus with Keytar'})}
className={styles.heroLogo}
src={useBaseUrl('/img/docusaurus_keytar.svg')}
img={useBaseUrl('/img/docusaurus_keytar.svg')}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ideal image plugin is not designed to work with SVGs

Eventually we could import the svgs and use them as components instead of using <img src="xyz.svg"/> like we do atm

Comment on lines +41 to +44
<Image
alt={name}
className={clsx('avatar__photo', styles.avatarImg)}
src={avatar}
img={avatar}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal image is not designed to work with external URLs. <img> is fine in this case

Comment on lines -36 to +40
<img
<Image
alt={name}
className="avatar__photo"
src={`https://unavatar.io/twitter/${handle}?fallback=https://github.com/${githubUsername}.png`}
img={`https://unavatar.io/twitter/${handle}?fallback=https://github.com/${githubUsername}.png`}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal image is not designed to work with external URLs. <img> is fine in this case

Comment on lines -43 to +46
<img
<Image
className="avatar__photo avatar__photo--xl"
src={`${githubUrl}.png`}
img={`${githubUrl}.png`}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal image is not designed to work with external URLs. <img> is fine in this case

Comment on lines -24 to +26
<img
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=353916&theme=light"
<Image
img="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=353916&theme=light"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal image is not designed to work with external URLs. <img> is fine in this case

@Devansu-Yadav
Copy link
Contributor Author

Devansu-Yadav commented Apr 8, 2023

Thanks but unfortunately that's not so simple. It is preferable that you open an RFC of the behavior you plan to implement before starting the work because the description of the original issue is not really complete and does not mention any design or edge cases to consider.

@slorber Apologies for underestimating the complexity of the implementation and misunderstanding the behavior of this eslint rule. I should have discussed the schema and the desired behavior first before implementing this rule 😅

I agree the original issue does lack more details regarding the design and edge cases. Actually, I saw one of the comments on the original issue that mentioned that we could directly link a PR back to this issue; hence I went ahead with implementing this PR.

We should allow any string link coming from props, dynamic or hardcoded values:

Sure, it makes sense, as I noticed that the internal implementation of IdealImage in Docusaurus just returns an <img> tag whenever we pass string links.

Eventually, add an option to reject relative local links?

Any particular reason why we would want to reject relative local links?

The only patterns that IMHO we can safely reject are the usage of images that are required/imported locally:

Got it 👍. So, do we want to encourage users to use the <IdealImage /> whenever images are required/imported locally? Also, if we encourage this behavior, do we also want to check whether the imported/required image is of a supported format or not and throw a violation through this rule?

You can get the real errors by turning disableInDev: false, in the plugin options

https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-ideal-image#disableInDev

err TypeError: Cannot read properties of undefined (reading 'src')
    at fallbackParams (main:8332:43)
    at IdealImage.render (main:7750:106)
    at d (main:67040:231)
    at bb (main:67041:16)
    at a.b.render (main:67047:43)
    at a.b.read (main:67046:83)
    at Object.exports.renderToString (main:67057:138)
    at doRender (main:219469:37)
    at async render (main:219387:16)
    at async /Users/sebastienlorber/Desktop/projects/docusaurus/node_modules/p-map/index.js:57:22

Got it 👍

This PR is not ready to merge, not because of the CI failures, but because the behavior implemented is not what we need.

I'll make the appropriate changes in this PR to fulfill the desired behavior of this eslint rule.

@slorber
Copy link
Collaborator

slorber commented Apr 13, 2023

@slorber Apologies for underestimating the complexity of the implementation and misunderstanding the behavior of this eslint rule. I should have discussed the schema and the desired behavior first before implementing this rule 😅

I agree the original issue does lack more details regarding the design and edge cases. Actually, I saw one of the comments on the original issue that mentioned that we could directly link a PR back to this issue; hence I went ahead with implementing this PR.

Not a big deal, some eslint rules are simples while others are much more complex. We can continue the design of the feature in this PR.

We should allow any string link coming from props, dynamic or hardcoded values:

Sure, it makes sense, as I noticed that the internal implementation of IdealImage in Docusaurus just returns an <img> tag whenever we pass string links.

That's not what I wanted to say. What I mean is that this should be valid because the user could use this component using absolute URLs like a github avatar link.

function Avatar({src}) {
  return <img src={src}/>
}

We can't know if the component is meant to be used with local images (that can be required) so it shouldn't error by default. At most it should be a warning IMHO, but probably default to disabled.

Eventually, add an option to reject relative local links?

Any particular reason why we would want to reject relative local links?

If an image is in the repo, it's probably more optimized to require it instead of using a string path.

I think it's good if we emit a warning by default (with option to disable) for these cases:

<img src="./xyz.png" />

<img src="/xyz.png" />

const imgSrc = "./xyz.png"
<img src={imgSrc} />

const imgSrc = "/xyz.png"
<img src={imgSrc} />

The only patterns that IMHO we can safely reject are the usage of images that are required/imported locally:

Got it 👍. So, do we want to encourage users to use the <IdealImage /> whenever images are required/imported locally?

Yes, by default we should error for these patterns and encourage usage of the <IdealImage/> component:

import imgSrc from './path/to/img.png';

<img src={imgSrc} />

<img src={require("./img.png")}/>

Also, if we encourage this behavior, do we also want to check whether the imported/required image is of a supported format or not and throw a violation through this rule?

You mean requiring avif, webp and other similar extensions?

I guess it's not really necessary, the Webpack loader will likely already throw an error and we'll likely want to support those extensions in the future.

I'll make the appropriate changes in this PR to fulfill the desired behavior of this eslint rule.

Thanks

I'm not very skilled in writing ESLint plugins, so can't really help to figure out how to check if image was locally required/imported. Maybe @Josh-Cena can guide you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Signed Facebook CLA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants