mirror of
https://github.com/reactjs/react.dev.git
synced 2026-02-21 19:31:57 +00:00
[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:
@@ -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
342
scripts/deadLinkChecker.js
Normal 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);
|
||||
});
|
||||
16
yarn.lock
16
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user