[be] Add deadlinks script (#7879)

While rewriting the compiler docs I happened to notice some deadlinks. This PR adds a new `yarn deadlinks` script to identify all deadlinks.

I decided to make this a script for now for simplicity but in the future could be ported to an ESlint rule.

The script handles:

- [x] checks images correctly (images are stored in /public but links can omit the /public)
- [x] looks up React error codes for dynamic error pages
- [x] lints links to contributors and uses URL from acknowledgements page if the member is no longer active on the core team
- [x] special injected anchor tags like #recap and #challenges

Example:

```
yarn run v1.22.22
$ node scripts/deadLinkChecker.js
Checking 177 markdown files...
Fetched 552 React error codes

src/content/learn/add-react-to-an-existing-project.md:23:58
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/add-react-to-an-existing-project.md:27:45
  Link text: benefit from the best practices
  URL: /learn/start-a-new-react-project#can-i-use-react-without-a-framework
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/add-react-to-an-existing-project.md:152:269
  Link text: a React framework
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/synchronizing-with-effects.md:735:18
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/typescript.md:16:3
  Link text: Common types from `@types/react`
  URL: /learn/typescript/#useful-types
  ✗ Target file not found for: /learn/typescript/

src/content/learn/typescript.md:17:3
  Link text: Further learning locations
  URL: /learn/typescript/#further-learning
  ✗ Target file not found for: /learn/typescript/

src/content/learn/typescript.md:23:5
  Link text: production-grade React frameworks
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/you-might-not-need-an-effect.md:29:399
  Link text: frameworks
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/you-might-not-need-an-effect.md:754:106
  Link text: frameworks
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/learn/your-first-component.md:218:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react/ViewTransition.md:146:248
  Link text: reveal content
  URL: /link-to-suspense-below
  ✗ Target file not found for: /link-to-suspense-below

src/content/reference/react/captureOwnerStack.md:60:94
  Link text: `errorInfo.componentStack` in `onUncaughtError`
  URL: /reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors
  ✗ Anchor #show-a-dialog-for-uncaught-errors not found in reference/react-dom/client/hydrateRoot.md

src/content/reference/react/forwardRef.md:9:65
  Link text: here
  URL: /blog/2024/04/25/react-19#ref-as-a-prop
  ✗ Target file not found for: /blog/2024/04/25/react-19

src/content/reference/react/use.md:315:24
  Link text: Server Component
  URL: /reference/react/components#server-components
  ✗ Anchor #server-components not found in reference/react/components.md

src/content/reference/react/useEffect.md:899:67
  Link text: if you use a framework,
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react/useEffect.md:1051:18
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react/useEffect.md:1736:92
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react/useInsertionEffect.md:136:65
  Link text: non-blocking update,
  URL: /reference/react/useTransition#marking-a-state-update-as-a-non-blocking-transition
  ✗ Anchor #marking-a-state-update-as-a-non-blocking-transition not found in reference/react/useTransition.md

src/content/reference/react-dom/createPortal.md:53:76
  Link text: key.
  URL: /learn/rendering-lists/#keeping-list-items-in-order-with-key
  ✗ Target file not found for: /learn/rendering-lists/

src/content/reference/react-dom/index.md:24:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/index.md:51:3
  Link text: `unmountComponentAtNode`
  URL: /reference/react-dom/unmountComponentAtNode
  ✗ Target file not found for: /reference/react-dom/unmountComponentAtNode

src/content/reference/react-dom/preinit.md:7:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/preinitModule.md:7:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/preload.md:7:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/preloadModule.md:7:1
  Link text: React-based frameworks
  URL: /learn/start-a-new-react-project
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/rsc/directives.md:13:36
  Link text: bundlers compatible with React Server Components
  URL: /learn/start-a-new-react-project#full-stack-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/rsc/server-components.md:7:34
  Link text: React Server Components
  URL: /learn/start-a-new-react-project#full-stack-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/rsc/server-functions.md:198:28
  Link text: `useActionState`
  URL: /reference/react-dom/hooks/useFormState
  ✗ Target file not found for: /reference/react-dom/hooks/useFormState

src/content/reference/rsc/server-functions.md:222:28
  Link text: `useActionState`
  URL: /reference/react-dom/hooks/useFormState
  ✗ Target file not found for: /reference/react-dom/hooks/useFormState

src/content/reference/rsc/use-client.md:44:77
  Link text: compatible bundlers
  URL: /learn/start-a-new-react-project#full-stack-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/rsc/use-server.md:98:54
  Link text: serializable props
  URL: /reference/rsc/use-client#passing-props-from-server-to-client-components
  ✗ Anchor #passing-props-from-server-to-client-components not found in reference/rsc/use-client.md

src/content/reference/react-dom/client/createRoot.md:212:278
  Link text: using a framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/client/index.md:7:185
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/components/common.md:919:89
  Link text: check out more examples.
  URL: /reference/react/useRef#examples-dom
  ✗ Anchor #examples-dom not found in reference/react/useRef.md

src/content/reference/react-dom/components/form.md:39:23
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/form.md:233:63
  Link text: reference documentation
  URL: /reference/react/hooks/useOptimistic
  ✗ Target file not found for: /reference/react/hooks/useOptimistic

src/content/reference/react-dom/components/input.md:33:24
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/link.md:33:23
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/meta.md:33:23
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/option.md:39:25
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/progress.md:33:27
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/script.md:34:25
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/select.md:39:25
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/style.md:33:24
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/textarea.md:33:27
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/components/title.md:33:24
  Link text: common element props.
  URL: /reference/react-dom/components/common#props
  ✗ Anchor #props not found in reference/react-dom/components/common.md

src/content/reference/react-dom/server/index.md:7:182
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/reference/react-dom/static/index.md:7:146
  Link text: framework
  URL: /learn/start-a-new-react-project#production-grade-react-frameworks
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/blog/2023/03/16/introducing-react-dev.md:45:5
  Link text: API Reference
  URL: /reference
  ✗ Target file not found for: /reference

src/content/blog/2023/03/16/introducing-react-dev.md:610:117
  Link text: Alternatives
  URL: /reference/react-dom/findDOMNode#alternatives
  ✗ Target file not found for: /reference/react-dom/findDOMNode

src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md:34:40
  Link text: Next.js App Router
  URL: /learn/start-a-new-react-project#nextjs-app-router
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023.md:95:605
  Link text: Next.js App Router
  URL: /learn/start-a-new-react-project#nextjs-app-router
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024.md:110:3
  Link text: Sathya Gunasekaran
  URL: /community/team#sathya-gunasekaran
  ✗ Contributor link should be updated to: https://github.com/gsathya

src/content/blog/2024/04/25/react-19-upgrade-guide.md:132:20
  Link text: improved how errors are handled
  URL: /blog/2024/04/25/react-19#error-handling
  ✗ Target file not found for: /blog/2024/04/25/react-19

src/content/blog/2024/04/25/react-19-upgrade-guide.md:502:19
  Link text: `ref` as a prop
  URL: /blog/2024/04/25/react-19#ref-as-a-prop
  ✗ Target file not found for: /blog/2024/04/25/react-19

src/content/blog/2024/12/05/react-19.md:358:391
  Link text: Full-stack React Architecture
  URL: /learn/start-a-new-react-project#which-features-make-up-the-react-teams-full-stack-architecture-vision
  ✗ Target file not found for: /learn/start-a-new-react-project

src/content/blog/2024/12/05/react-19.md:392:28
  Link text: React Server Actions
  URL: /reference/rsc/server-actions
  ✗ Target file not found for: /reference/rsc/server-actions

src/content/blog/2025/04/23/react-labs-view-transitions-activity-and-more.md:2498:37
  Link text: view transition classes
  URL: /reference/react/ViewTransition#view-transition-classes
  ✗ Anchor #view-transition-classes not found in reference/react/ViewTransition.md


Found 58 dead links out of 1555 total links

info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

```
This commit is contained in:
lauren
2025-07-18 17:19:40 -04:00
committed by GitHub
parent 84a56968d9
commit e245b77694
3 changed files with 354 additions and 10 deletions

View File

@@ -15,12 +15,13 @@
"prettier:diff": "yarn nit:source",
"lint-heading-ids": "node scripts/headingIdLinter.js",
"fix-headings": "node scripts/headingIdLinter.js --fix",
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss",
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks",
"tsc": "tsc --noEmit",
"start": "next start",
"postinstall": "is-ci || husky install .husky",
"check-all": "npm-run-all prettier lint:fix tsc rss",
"rss": "node scripts/generateRss.js"
"rss": "node scripts/generateRss.js",
"deadlinks": "node scripts/deadLinkChecker.js"
},
"dependencies": {
"@codesandbox/sandpack-react": "2.13.5",
@@ -61,6 +62,7 @@
"autoprefixer": "^10.4.2",
"babel-eslint": "10.x",
"babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112",
"chalk": "4.1.2",
"eslint": "7.x",
"eslint-config-next": "12.0.3",
"eslint-config-react-app": "^5.2.1",

342
scripts/deadLinkChecker.js Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const globby = require('globby');
const chalk = require('chalk');
const CONTENT_DIR = path.join(__dirname, '../src/content');
const PUBLIC_DIR = path.join(__dirname, '../public');
const fileCache = new Map();
const anchorMap = new Map(); // Map<filepath, Set<anchorId>>
const contributorMap = new Map(); // Map<anchorId, URL>
let errorCodes = new Set();
async function readFileWithCache(filePath) {
if (!fileCache.has(filePath)) {
try {
const content = await fs.promises.readFile(filePath, 'utf8');
fileCache.set(filePath, content);
} catch (error) {
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
}
}
return fileCache.get(filePath);
}
async function fileExists(filePath) {
try {
await fs.promises.access(filePath, fs.constants.R_OK);
return true;
} catch {
return false;
}
}
function getMarkdownFiles() {
// Convert Windows paths to POSIX for globby compatibility
const baseDir = CONTENT_DIR.replace(/\\/g, '/');
const patterns = [
path.posix.join(baseDir, '**/*.md'),
path.posix.join(baseDir, '**/*.mdx'),
];
return globby.sync(patterns);
}
function extractAnchorsFromContent(content) {
const anchors = new Set();
// MDX-style heading IDs: {/*anchor-id*/}
const mdxPattern = /\{\/\*([a-zA-Z0-9-_]+)\*\/\}/g;
let match;
while ((match = mdxPattern.exec(content)) !== null) {
anchors.add(match[1].toLowerCase());
}
// HTML id attributes
const htmlIdPattern = /\sid=["']([a-zA-Z0-9-_]+)["']/g;
while ((match = htmlIdPattern.exec(content)) !== null) {
anchors.add(match[1].toLowerCase());
}
// Markdown heading with explicit ID: ## Heading {#anchor-id}
const markdownHeadingPattern = /^#+\s+.*\{#([a-zA-Z0-9-_]+)\}/gm;
while ((match = markdownHeadingPattern.exec(content)) !== null) {
anchors.add(match[1].toLowerCase());
}
return anchors;
}
async function buildAnchorMap(files) {
for (const filePath of files) {
const content = await readFileWithCache(filePath);
const anchors = extractAnchorsFromContent(content);
if (anchors.size > 0) {
anchorMap.set(filePath, anchors);
}
}
}
function extractLinksFromContent(content) {
const linkPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
const links = [];
let match;
while ((match = linkPattern.exec(content)) !== null) {
const [, linkText, linkUrl] = match;
if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) {
const lines = content.substring(0, match.index).split('\n');
const line = lines.length;
const lastLineStart =
lines.length > 1 ? content.lastIndexOf('\n', match.index - 1) + 1 : 0;
const column = match.index - lastLineStart + 1;
links.push({
text: linkText,
url: linkUrl,
line,
column,
});
}
}
return links;
}
async function findTargetFile(urlPath) {
// Check if it's an image or static asset that might be in the public directory
const imageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.ico',
'.webp',
];
const hasImageExtension = imageExtensions.some((ext) =>
urlPath.toLowerCase().endsWith(ext)
);
if (hasImageExtension || urlPath.includes('.')) {
// Check in public directory (with and without leading slash)
const publicPaths = [
path.join(PUBLIC_DIR, urlPath),
path.join(PUBLIC_DIR, urlPath.substring(1)),
];
for (const p of publicPaths) {
if (await fileExists(p)) {
return p;
}
}
}
const possiblePaths = [
path.join(CONTENT_DIR, urlPath + '.md'),
path.join(CONTENT_DIR, urlPath + '.mdx'),
path.join(CONTENT_DIR, urlPath, 'index.md'),
path.join(CONTENT_DIR, urlPath, 'index.mdx'),
// Without leading slash
path.join(CONTENT_DIR, urlPath.substring(1) + '.md'),
path.join(CONTENT_DIR, urlPath.substring(1) + '.mdx'),
path.join(CONTENT_DIR, urlPath.substring(1), 'index.md'),
path.join(CONTENT_DIR, urlPath.substring(1), 'index.mdx'),
];
for (const p of possiblePaths) {
if (await fileExists(p)) {
return p;
}
}
return null;
}
async function validateLink(link) {
const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/;
const anchorMatch = link.url.match(urlAnchorPattern);
const urlWithoutAnchor = link.url.replace(urlAnchorPattern, '');
if (urlWithoutAnchor === '/') {
return {valid: true};
}
// Check if it's an error code link
const errorCodeMatch = urlWithoutAnchor.match(/^\/errors\/(\d+)$/);
if (errorCodeMatch) {
const code = errorCodeMatch[1];
if (!errorCodes.has(code)) {
return {
valid: false,
reason: `Error code ${code} not found in React error codes`,
};
}
return {valid: true};
}
// Check if it's a contributor link on the team or acknowledgements page
if (
anchorMatch &&
(urlWithoutAnchor === '/community/team' ||
urlWithoutAnchor === '/community/acknowledgements')
) {
const anchorId = anchorMatch[1].toLowerCase();
if (contributorMap.has(anchorId)) {
const correctUrl = contributorMap.get(anchorId);
if (correctUrl !== link.url) {
return {
valid: false,
reason: `Contributor link should be updated to: ${correctUrl}`,
};
}
return {valid: true};
} else {
return {
valid: false,
reason: `Contributor link not found`,
};
}
}
const targetFile = await findTargetFile(urlWithoutAnchor);
if (!targetFile) {
return {
valid: false,
reason: `Target file not found for: ${urlWithoutAnchor}`,
};
}
// Only check anchors for content files, not static assets
if (anchorMatch && targetFile.startsWith(CONTENT_DIR)) {
const anchorId = anchorMatch[1].toLowerCase();
// TODO handle more special cases. These are usually from custom MDX components that include
// a Heading from src/components/MDX/Heading.tsx which automatically injects an anchor tag.
switch (anchorId) {
case 'challenges':
case 'recap': {
return {valid: true};
}
}
const fileAnchors = anchorMap.get(targetFile);
if (!fileAnchors || !fileAnchors.has(anchorId)) {
return {
valid: false,
reason: `Anchor #${anchorMatch[1]} not found in ${path.relative(
CONTENT_DIR,
targetFile
)}`,
};
}
}
return {valid: true};
}
async function processFile(filePath) {
const content = await readFileWithCache(filePath);
const links = extractLinksFromContent(content);
const deadLinks = [];
for (const link of links) {
const result = await validateLink(link);
if (!result.valid) {
deadLinks.push({
file: path.relative(process.cwd(), filePath),
line: link.line,
column: link.column,
text: link.text,
url: link.url,
reason: result.reason,
});
}
}
return {deadLinks, totalLinks: links.length};
}
async function buildContributorMap() {
const teamFile = path.join(CONTENT_DIR, 'community/team.md');
const teamContent = await readFileWithCache(teamFile);
const teamMemberPattern = /<TeamMember[^>]*permalink=["']([^"']+)["']/g;
let match;
while ((match = teamMemberPattern.exec(teamContent)) !== null) {
const permalink = match[1];
contributorMap.set(permalink, `/community/team#${permalink}`);
}
const ackFile = path.join(CONTENT_DIR, 'community/acknowledgements.md');
const ackContent = await readFileWithCache(ackFile);
const contributorPattern = /\*\s*\[([^\]]+)\]\(([^)]+)\)/g;
while ((match = contributorPattern.exec(ackContent)) !== null) {
const name = match[1];
const url = match[2];
const hyphenatedName = name.toLowerCase().replace(/\s+/g, '-');
if (!contributorMap.has(hyphenatedName)) {
contributorMap.set(hyphenatedName, url);
}
}
}
async function fetchErrorCodes() {
try {
const response = await fetch(
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
);
if (!response.ok) {
throw new Error(`Failed to fetch error codes: ${response.status}`);
}
const codes = await response.json();
errorCodes = new Set(Object.keys(codes));
console.log(chalk.gray(`Fetched ${errorCodes.size} React error codes\n`));
} catch (error) {
throw new Error(`Failed to fetch error codes: ${error.message}`);
}
}
async function main() {
const files = getMarkdownFiles();
console.log(chalk.gray(`Checking ${files.length} markdown files...`));
await fetchErrorCodes();
await buildContributorMap();
await buildAnchorMap(files);
const filePromises = files.map((filePath) => processFile(filePath));
const results = await Promise.all(filePromises);
const deadLinks = results.flatMap((r) => r.deadLinks);
const totalLinks = results.reduce((sum, r) => sum + r.totalLinks, 0);
if (deadLinks.length > 0) {
for (const link of deadLinks) {
console.log(chalk.yellow(`${link.file}:${link.line}:${link.column}`));
console.log(chalk.reset(` Link text: ${link.text}`));
console.log(chalk.reset(` URL: ${link.url}`));
console.log(` ${chalk.red('✗')} ${chalk.red(link.reason)}\n`);
}
console.log(
chalk.red(
`\nFound ${deadLinks.length} dead link${
deadLinks.length > 1 ? 's' : ''
} out of ${totalLinks} total links\n`
)
);
process.exit(1);
}
console.log(chalk.green(`\n✓ All ${totalLinks} links are valid!\n`));
process.exit(0);
}
main().catch((error) => {
console.log(chalk.red(`Error: ${error.message}`));
process.exit(1);
});

View File

@@ -2443,6 +2443,14 @@ ccount@^2.0.0:
resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^2.0.0, chalk@^2.4.1:
version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
@@ -2452,14 +2460,6 @@ chalk@^2.0.0, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
character-entities-html4@^1.0.0:
version "1.1.4"
resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz"