Compare commits

..

4 Commits

Author SHA1 Message Date
github-actions[bot]
c8082fbac8 Version Packages 2022-08-24 11:37:04 +02:00
Thomas Allmer
68e05f4d4a feat(check-html-links): handle as internal if starts with a provided absolute base url 2022-08-24 11:33:10 +02:00
Thomas Allmer
660f64c320 fix(launch): change the default git branch to main 2022-08-24 11:33:10 +02:00
Jorge del Casar
97d5fb2040 feat(check-html-links): add external link validation 2022-08-24 11:33:10 +02:00
26 changed files with 360 additions and 59 deletions

View File

@@ -81,7 +81,7 @@
"husky": "^4.3.7",
"lint-staged": "^10.5.3",
"mocha": "^9.1.3",
"node-fetch": "^2.6.7",
"node-fetch": "^3.0.0",
"npm-run-all": "^4.1.5",
"onchange": "^7.1.0",
"prettier": "^2.5.1",

View File

@@ -1,5 +1,21 @@
# check-html-links
## 0.2.4
### Patch Changes
- 97d5fb2: Add external links validation via the flag `--validate-externals`.
You can/should provide an optional `--absolute-base-url` to handle urls starting with it as internal.
```bash
# check external urls
npx check-html-links _site --validate-externals
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev
```
## 0.2.3
### Patch Changes

View File

@@ -14,7 +14,7 @@ npm i -D check-html-links
npx check-html-links _site
```
For docs please see our homepage [https://rocket.modern-web.dev/docs/tools/check-html-links/](https://rocket.modern-web.dev/docs/tools/check-html-links/).
For docs please see our homepage [https://rocket.modern-web.dev/tools/check-html-links/overview/](https://rocket.modern-web.dev/tools/check-html-links/overview/).
## Comparison

View File

@@ -1,6 +1,6 @@
{
"name": "check-html-links",
"version": "0.2.3",
"version": "0.2.4",
"publishConfig": {
"access": "public"
},
@@ -37,6 +37,7 @@
"command-line-args": "^5.1.1",
"glob": "^7.0.0",
"minimatch": "^3.0.4",
"node-fetch": "^3.0.0",
"sax-wasm": "^2.0.0",
"slash": "^4.0.0"
},

View File

@@ -6,7 +6,7 @@ import path from 'path';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import { validateFiles } from './validateFolder.js';
import { prepareFiles, validateFiles } from './validateFolder.js';
import { formatErrors } from './formatErrors.js';
import { listFiles } from './listFiles.js';
@@ -18,7 +18,9 @@ export class CheckHtmlLinksCli {
const mainDefinitions = [
{ name: 'ignore-link-pattern', type: String, multiple: true },
{ name: 'root-dir', type: String, defaultOption: true },
{ name: 'continue-on-error', type: Boolean, defaultOption: false },
{ name: 'continue-on-error', type: Boolean },
{ name: 'validate-externals', type: Boolean },
{ name: 'absolute-base-url', type: String },
];
const options = commandLineArgs(mainDefinitions, {
stopAtFirstUnknown: true,
@@ -29,6 +31,8 @@ export class CheckHtmlLinksCli {
continueOnError: options['continue-on-error'],
rootDir: options['root-dir'],
ignoreLinkPatterns: options['ignore-link-pattern'],
validateExternals: options['validate-externals'],
absoluteBaseUrl: options['absolute-base-url'],
};
}
@@ -43,22 +47,47 @@ export class CheckHtmlLinksCli {
}
async run() {
const { ignoreLinkPatterns, rootDir: userRootDir } = this.options;
const {
ignoreLinkPatterns,
rootDir: userRootDir,
validateExternals,
absoluteBaseUrl,
} = this.options;
const rootDir = userRootDir ? path.resolve(userRootDir) : process.cwd();
const performanceStart = process.hrtime();
console.log('👀 Checking if all internal links work...');
const files = await listFiles('**/*.html', rootDir);
console.log('Check HTML Links');
const filesOutput =
files.length == 0
? '🧐 No files to check. Did you select the correct folder?'
: `🔥 Found a total of ${chalk.green.bold(files.length)} files to check!`;
? ' 🧐 No files to check. Did you select the correct folder?'
: ` 📖 Found ${chalk.green.bold(files.length)} files containing`;
console.log(filesOutput);
const { errors, numberLinks } = await validateFiles(files, rootDir, { ignoreLinkPatterns });
const { numberLinks, checkLocalFiles, checkExternalLinks } = await prepareFiles(
files,
rootDir,
{
ignoreLinkPatterns,
validateExternals,
absoluteBaseUrl,
},
);
console.log(`🔗 Found a total of ${chalk.green.bold(numberLinks)} links to validate!\n`);
console.log(` 🔗 ${chalk.green.bold(numberLinks)} internal links`);
if (validateExternals) {
console.log(` 🌐 ${chalk.green.bold(checkExternalLinks.length)} external links`);
}
console.log(' 👀 Checking...');
const { errors } = await validateFiles({
checkLocalFiles,
validateExternals,
checkExternalLinks,
});
const performance = process.hrtime(performanceStart);
/** @type {string[]} */
@@ -70,7 +99,7 @@ export class CheckHtmlLinksCli {
referenceCount += error.usage.length;
}
output = [
`❌ Found ${chalk.red.bold(
` ❌ Found ${chalk.red.bold(
errors.length.toString(),
)} missing reference targets (used by ${referenceCount} links) while checking ${
files.length
@@ -78,7 +107,7 @@ export class CheckHtmlLinksCli {
...formatErrors(errors)
.split('\n')
.map(line => ` ${line}`),
`Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
` 🕑 Checking links duration: ${performance[0]}s ${performance[1] / 1000000}ms`,
];
message = output.join('\n');
if (this.options.printOnError === true) {
@@ -89,7 +118,7 @@ export class CheckHtmlLinksCli {
}
} else {
console.log(
`✅ All internal links are valid. (executed in ${performance[0]}s ${
` ✅ All tested links are valid. (executed in ${performance[0]}s ${
performance[1] / 1000000
}ms)`,
);

View File

@@ -0,0 +1,63 @@
import fetch from 'node-fetch';
/**
* @type {Map<string,boolean>}
*/
const resultsMap = new Map();
/**
*
* @param {string} url
* @param {boolean} result
* @returns {boolean}
*/
const memorizeCheckup = (url, result) => {
resultsMap.set(url, result);
return result;
};
/**
*
* @param {string} url
* @param {string} method
* @returns
*/
const fetchWrap = async (url, method = 'GET') => {
return Promise.race([
fetch(url, { method })
.then(response => response.ok)
.catch(() => false),
new Promise(resolve => setTimeout(resolve, 10000, false)),
]);
};
/**
*
* @param {string} url
* @returns {Promise<boolean>}
*/
const fetchHead = async url => fetchWrap(url, 'HEAD');
/**
*
* @param {string} url - URL object to check
* @returns {Promise<boolean>} true if url is alive or false if not
*/
const checkUrl = async url =>
(fetchHead(url) || fetchWrap(url)).then(result => memorizeCheckup(url, result));
/**
*
* @param {string} link - link string to check
* @returns {Promise<boolean>}
*/
export const checkLink = async link => {
const url = link.startsWith('//') ? `https:${link}` : link;
return resultsMap.get(url) ?? checkUrl(url);
};
/**
* Check an array of links and return an object with
*
* @param {string[]} links Links to check
*/
export const checkLinks = async links => Promise.all(links.map(checkLink));

View File

@@ -15,7 +15,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const filePath = path.relative(relativeFrom, error.filePath);
if (error.onlyAnchorMissing === true) {
output.push(
`${number}. missing ${chalk.red.bold(
` ${number}. missing ${chalk.red.bold(
`id="${error.usage[0].anchor}"`,
)} in ${chalk.cyanBright(filePath)}`,
);
@@ -24,7 +24,7 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const title =
firstAttribute === 'src' || firstAttribute === 'srcset' ? 'file' : 'reference target';
output.push(`${number}. missing ${title} ${chalk.red.bold(filePath)}`);
output.push(` ${number}. missing ${title} ${chalk.red.bold(filePath)}`);
}
const usageLength = error.usage.length;
@@ -34,11 +34,11 @@ export function formatErrors(errors, relativeFrom = process.cwd()) {
const clickAbleLink = chalk.cyanBright(`${usagePath}:${usage.line + 1}:${usage.character}`);
const attributeStart = chalk.gray(`${usage.attribute}="`);
const attributeEnd = chalk.gray('"');
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
output.push(` from ${clickAbleLink} via ${attributeStart}${usage.value}${attributeEnd}`);
}
if (usageLength > 3) {
const more = chalk.red((usageLength - 3).toString());
output.push(` ... ${more} more references to this target`);
output.push(` ... ${more} more references to this target`);
}
output.push('');
}

View File

@@ -3,10 +3,10 @@ import fs from 'fs';
import saxWasm from 'sax-wasm';
import minimatch from 'minimatch';
import { createRequire } from 'module';
import { listFiles } from './listFiles.js';
import path from 'path';
import slash from 'slash';
import { listFiles } from './listFiles.js';
import { checkLinks } from './checkLinks.js';
/** @typedef {import('../types/main').Link} Link */
/** @typedef {import('../types/main').LocalFile} LocalFile */
@@ -28,6 +28,9 @@ const parserIds = new SAXParser(SaxEventType.Attribute, streamOptions);
/** @type {Error[]} */
let checkLocalFiles = [];
/** @type {Error[]} */
let checkExternalLinks = [];
/** @type {Error[]} */
let errors = [];
@@ -151,6 +154,26 @@ function addLocalFile(filePath, anchor, usageObj) {
}
}
/**
* @param {string} filePath
* @param {Usage} usageObj
*/
function addExternalLink(filePath, usageObj) {
const foundIndex = checkExternalLinks.findIndex(item => {
return item.filePath === filePath;
});
if (foundIndex === -1) {
checkExternalLinks.push({
filePath,
onlyAnchorMissing: false,
usage: [usageObj],
});
} else {
checkExternalLinks[foundIndex].usage.push(usageObj);
}
}
/**
* @param {string} inValue
*/
@@ -200,11 +223,16 @@ function isNonHttpSchema(url) {
* @param {object} options
* @param {string} options.htmlFilePath
* @param {string} options.rootDir
* @param {string} options.absoluteBaseUrl
* @param {function(string): boolean} options.ignoreUsage
*/
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage, absoluteBaseUrl }) {
for (const hrefObj of links) {
const { value, anchor } = getValueAndAnchor(hrefObj.value);
const { value: rawValue, anchor } = getValueAndAnchor(hrefObj.value);
const value = rawValue.startsWith(absoluteBaseUrl)
? rawValue.substring(absoluteBaseUrl.length)
: rawValue;
const usageObj = {
attribute: hrefObj.attribute,
@@ -229,8 +257,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
} else if (valueFile === '' && anchor !== '') {
addLocalFile(htmlFilePath, anchor, usageObj);
} else if (value.startsWith('//') || value.startsWith('http')) {
// TODO: handle external urls
// external url - we do not handle that (yet)
addExternalLink(htmlFilePath, usageObj);
} else if (value.startsWith('/')) {
const filePath = path.join(rootDir, valueFile);
addLocalFile(filePath, anchor, usageObj);
@@ -244,7 +271,7 @@ async function resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage }) {
}
}
return { checkLocalFiles: [...checkLocalFiles] };
return { checkLocalFiles: [...checkLocalFiles], checkExternalLinks: [...checkExternalLinks] };
}
/**
@@ -283,17 +310,34 @@ async function validateLocalFiles(checkLocalFiles) {
return errors;
}
/**
*
* @param {Error[]} checkExternalLinks
*/
async function validateExternalLinks(checkExternalLinks) {
for await (const localFileObj of checkExternalLinks) {
const links = localFileObj.usage.map(usage => usage.value);
const results = await checkLinks(links);
localFileObj.usage = localFileObj.usage.filter((link, index) => !results[index]);
if (localFileObj.usage.length > 0) {
errors.push(localFileObj);
}
}
return errors;
}
/**
* @param {string[]} files
* @param {string} rootDir
* @param {Options} opts?
*/
export async function validateFiles(files, rootDir, opts) {
export async function prepareFiles(files, rootDir, opts) {
await parserReferences.prepareWasm(saxWasmBuffer);
await parserIds.prepareWasm(saxWasmBuffer);
errors = [];
checkLocalFiles = [];
checkExternalLinks = [];
idCache = new Map();
let numberLinks = 0;
@@ -309,11 +353,27 @@ export async function validateFiles(files, rootDir, opts) {
for (const htmlFilePath of files) {
const { links } = await extractReferences(htmlFilePath);
numberLinks += links.length;
await resolveLinks(links, { htmlFilePath, rootDir, ignoreUsage });
await resolveLinks(links, {
htmlFilePath,
rootDir,
ignoreUsage,
absoluteBaseUrl: opts?.absoluteBaseUrl,
});
}
await validateLocalFiles(checkLocalFiles);
return { checkLocalFiles, checkExternalLinks, numberLinks };
}
return { errors: errors, numberLinks: numberLinks };
/**
* @param {*} param0
* @returns
*/
export async function validateFiles({ checkLocalFiles, validateExternals, checkExternalLinks }) {
await validateLocalFiles(checkLocalFiles);
if (validateExternals) {
await validateExternalLinks(checkExternalLinks);
}
return { errors };
}
/**
@@ -323,6 +383,14 @@ export async function validateFiles(files, rootDir, opts) {
export async function validateFolder(inRootDir, opts) {
const rootDir = path.resolve(inRootDir);
const files = await listFiles('**/*.html', rootDir);
const { errors } = await validateFiles(files, rootDir, opts);
const { checkLocalFiles, checkExternalLinks } = await prepareFiles(files, rootDir, opts);
const { errors } = await validateFiles({
checkLocalFiles,
validateExternals: opts?.validateExternals,
checkExternalLinks,
});
return errors;
}

View File

@@ -1,5 +1,9 @@
<!-- ignore known subsystems -->
<a href="/docs/"></a>
<a href="/developer/getting-started.html#js"></a>
<a href="/developer/language-guides/"></a>
<a href="/developer/javascript.html"></a>
<!-- valid -->
<a href="//rocket.modern-web.dev/"></a>
<a href="http://rocket.modern-web.dev/"></a>
<a href="https://rocket.modern-web.dev/"></a>
<!-- invalid -->
<a href="//rocket.modern-web.dev/unexists-page/"></a>
<a href="http://rocket.modern-web.dev/unexists-page/"></a>
<a href="https://rocket.modern-web.dev/unexists-page/"></a>

View File

@@ -5,8 +5,5 @@
<a href="./page.html"></a>
<a href=" ./page.html "></a>
<a href=" /page.html "></a>
<a href="//domain.com/something/"></a>
<a href="http://domain.com/something/"></a>
<a href="https://domain.com/something/"></a>
<a href=""></a>
<a href=":~:text=put%20your%20labels%20above%20your%20inputs">Sign-in form best practices</a>

View File

@@ -0,0 +1,2 @@
<a href="about.html">About</a>
<a href="http://localhost/about.html">About Absolute</a>

View File

@@ -20,19 +20,19 @@ describe('formatErrors', () => {
const result = await executeAndFormat('fixtures/test-case');
expect(result.trim().split('\n')).to.deep.equal([
'1. missing id="my-teams" in fixtures/test-case/price/index.html',
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
' from fixtures/test-case/history/index.html:1:9 via href="/price/#my-teams"',
'',
'2. missing file fixtures/test-case/about/images/team.png',
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
' 2. missing file fixtures/test-case/about/images/team.png',
' from fixtures/test-case/about/index.html:3:10 via src="./images/team.png"',
'',
'3. missing reference target fixtures/test-case/aboot',
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
' ... 2 more references to this target',
' 3. missing reference target fixtures/test-case/aboot',
' from fixtures/test-case/about/index.html:6:11 via href="/aboot"',
' from fixtures/test-case/history/index.html:4:11 via href="/aboot"',
' from fixtures/test-case/index.html:4:11 via href="/aboot"',
' ... 2 more references to this target',
'',
'4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
' 4. missing reference target fixtures/test-case/prce',
' from fixtures/test-case/index.html:1:9 via href="./prce"',
]);
});
});

View File

@@ -66,6 +66,74 @@ describe('validateFolder', () => {
]);
});
it('validates external links', async () => {
const { errors, cleanup } = await execute('fixtures/external-link', {
validateExternals: true,
});
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/external-link/index.html',
onlyAnchorMissing: false,
usage: [
{
attribute: 'href',
value: '//rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 6,
character: 9,
anchor: '',
},
{
attribute: 'href',
value: 'http://rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 7,
character: 9,
anchor: '',
},
{
attribute: 'href',
value: 'https://rocket.modern-web.dev/unexists-page/',
file: 'fixtures/external-link/index.html',
line: 8,
character: 9,
anchor: '',
},
],
},
]);
});
it('validates links with own absolute base url as internal', async () => {
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
validateExternals: true,
absoluteBaseUrl: 'http://localhost',
});
expect(cleanup(errors)).to.deep.equal([]);
});
it('validates all full urls if there is no absoluteBaseUrl provided', async () => {
const { errors, cleanup } = await execute('fixtures/internal-own-absolute-base-path', {
validateExternals: true,
});
expect(cleanup(errors)).to.deep.equal([
{
filePath: 'fixtures/internal-own-absolute-base-path/index.html',
onlyAnchorMissing: false,
usage: [
{
anchor: '',
attribute: 'href',
character: 9,
file: 'fixtures/internal-own-absolute-base-path/index.html',
line: 1,
value: 'http://localhost/about.html',
},
],
},
]);
});
it('groups multiple usage of the same missing file', async () => {
const { errors, cleanup } = await execute('fixtures/internal-links-to-same-file');
expect(cleanup(errors)).to.deep.equal([

View File

@@ -26,13 +26,15 @@ export interface Error {
usage: Usage[];
}
interface Options {
export interface Options {
ignoreLinkPatterns: string[] | null;
validateExternals: boolean;
absoluteBaseUrl: string;
}
export interface CheckHtmlLinksCliOptions {
export interface CheckHtmlLinksCliOptions extends Options {
printOnError: boolean;
rootDir: string;
ignoreLinkPatterns: string[] | null;
continueOnError: boolean;
absoluteBaseUrl: string;
}

View File

@@ -1,5 +1,20 @@
# @rocket/cli
## 0.20.4
### Patch Changes
- 68e05f4: `rocket lint` can now validate external links.
Example:
```
rocket lint --validate-externals
```
- Updated dependencies [97d5fb2]
- check-html-links@0.2.4
## 0.20.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/cli",
"version": "0.20.3",
"version": "0.20.4",
"publishConfig": {
"access": "public"
},
@@ -54,7 +54,7 @@
"dependencies": {
"@rocket/building-rollup": "^0.4.0",
"@rocket/engine": "^0.2.7",
"check-html-links": "^0.2.3",
"check-html-links": "^0.2.4",
"colorette": "^2.0.16",
"commander": "^9.0.0",
"fs-extra": "^9.0.1",

View File

@@ -24,6 +24,7 @@ export class RocketLint {
.command('lint')
.option('-i, --input-dir <path>', 'path to where to search for source files')
.option('-b, --build-html', 'do a quick html only build and then check links')
.option('-e, --validate-externals', 'validate external links')
.action(async options => {
const { cliOptions, ...lintOptions } = options;
cli.setOptions({
@@ -77,6 +78,7 @@ export class RocketLint {
rootDir: folderToCheck,
printOnError: true,
continueOnError: false,
absoluteBaseUrl: this.cli.options.absoluteBaseUrl,
...userCheckHtmlLinksOptions,
});

View File

@@ -1,5 +1,13 @@
# @rocket/launch
## 0.21.3
### Patch Changes
- 660f64c: Change the default gitBranch to main
- Updated dependencies [68e05f4]
- @rocket/cli@0.20.4
## 0.21.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@rocket/launch",
"version": "0.21.2",
"version": "0.21.3",
"publishConfig": {
"access": "public"
},
@@ -46,7 +46,7 @@
"preset"
],
"dependencies": {
"@rocket/cli": "^0.20.3",
"@rocket/cli": "^0.20.4",
"@rocket/components": "^0.2.0",
"@rocket/engine": "^0.2.7",
"@webcomponents/template-shadowroot": "^0.1.0",

View File

@@ -57,7 +57,7 @@ export class LayoutMain extends Layout {
</picture>
`,
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
gitBranch: 'next',
gitBranch: 'main',
description: 'A static site generator for modern web development',
socialLinks: [
{

View File

@@ -3,6 +3,6 @@ export const defaultGlobalOptions = {
logoSrc: '/icon.svg',
logoAlt: 'Rocket Logo',
gitSiteUrl: 'https://github.com/modernweb-dev/rocket',
gitBranch: 'master',
gitBranch: 'main',
description: 'A static site generator for modern web development',
};

View File

@@ -68,6 +68,18 @@ rocket lint --build-html
Note: We can do this as 2-4 generally does not impact links/references (as long as the optimizations scripts do not have related bugs)
### Check external links
`rocket lint` can also check external links.
This will do a HEAD/GET request to every external link which means if all is green that the website only links to still active websites.
It however also means that it will do a lot of network requests which will take a while.
Use with care.
```bash
rocket lint --validate-externals
```
## Add a Not Found Page
When a user enters a URL that does not exist, a "famous" 404 Page Not Found error occurs.

View File

@@ -60,6 +60,8 @@ npm i -D check-html-links
| root-dir | string | the root directory to serve files from. Defaults to the current working directory |
| ignore-link-pattern | string | do not check links matching the pattern |
| continue-on-error | boolean | if present it will not exit with an error code - useful while writing or for temporary passing a ci |
| validate-externals | boolean | if present it will validate external links making a request to the linked url |
| absolute-base-url | string | the urls of your website - if provided it will handle absolute links that start with it as internal |
## Usage Examples
@@ -72,6 +74,12 @@ npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
# ignore all links like <a href="/users/123"> & <a href="/users/123/details">
npx check-html-links _site --ignore-link-pattern "/users/*" "/users/**/*"
# check external urls
npx check-html-links _site --validate-externals
# check external urls but treat links like https://rocket.modern-web.dev/about/ as internal
npx check-html-links _site --validate-externals --absolute-base-url https://rocket.modern-web.dev
```
## Example Output

View File

@@ -1373,6 +1373,11 @@
"id": "fixing-broken-links",
"level": 3
},
{
"text": "Check external links",
"id": "check-external-links",
"level": 3
},
{
"text": "Add a Not Found Page",
"id": "add-a-not-found-page",

View File

@@ -6109,14 +6109,14 @@ node-emoji@^1.10.0:
dependencies:
lodash "^4.17.21"
node-fetch@2.6.7, node-fetch@^2.6.7:
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.2.8:
node-fetch@^3.0.0, node-fetch@^3.2.8:
version "3.2.10"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==