Skip to content

Sync with react.dev @ d52b3ec7 #1032

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9db23d6
fix: correct broken WAI-ARIA modal dialog link in createPortal refere…
dimatitov Jun 2, 2025
bbcb9af
Update meetups.md adding React Rajasthan Community (#7831)
shubhamui Jun 2, 2025
a2d17d1
Update components-and-hooks-must-be-pure.md (#7830)
ExercitusMortem Jun 2, 2025
94424ae
Update referencing-values-with-refs.md (#7829)
cHaLkdusT Jun 2, 2025
172f0b9
Add uwu click animation (#7822)
Jinsoo1004 Jun 2, 2025
3dcc4c4
Fix typo and clarily that a server function reference is created only…
kapantzak Jun 2, 2025
06965de
Add React Alicante 2025 to Conferences page (#7674)
mikedidomizio Jun 2, 2025
e901790
fix: use const where applicable in examples for keeping components pu…
ad1992 Jun 2, 2025
87cef4a
Remove `forwardRef` reference from API listing (#7837)
kassens Jun 3, 2025
c60173f
docs: Refactor context provider usage (#7793)
nannany Jun 3, 2025
37b09ea
fix: typo in docs on prerendering (#7823)
yeskunall Jun 3, 2025
5927c4e
Replace Context.Provider with Context (#7838)
kassens Jun 3, 2025
5dca520
fix(blog): resolve typo in React 19 blog post (`refs` → `ref`s) (#7828)
amir78729 Jun 3, 2025
50d6991
Update analyze_comment.yml (#7840)
jtn-dev Jun 6, 2025
82f2863
Fix #6915: typo fix (#6917)
Rekl0w Jun 28, 2025
741e8d9
fix: update ids to point to right part of the docs (#7854)
yeskunall Jun 28, 2025
c0c955e
chore: remove unused date-fns (#7856)
noritaka1166 Jun 28, 2025
b79ad22
chore: fix typo in resource and metadata components documentation (#7…
Rekl0w Jul 2, 2025
341c312
fix: correct typo in scaling-up-with-reducer-and-context.md (#7390)
bcdipesh Jul 2, 2025
4846020
fix flushSync link (#7862)
rickhanlonii Jul 9, 2025
84a5696
docs(react): fix grammar in forward ref deprecation message (#7864)
SimonSchick Jul 10, 2025
e245b77
[be] Add deadlinks script (#7879)
poteto Jul 18, 2025
d52b3ec
Fix deadlinks (#7880)
poteto Jul 18, 2025
4b8d639
merging all conflicts
react-translations-bot Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/analyze_comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ on:
types:
- completed

permissions: {}

permissions:
contents: read
issues: write
pull-requests: write

jobs:
comment:
runs-on: ubuntu-latest
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,7 +31,6 @@
"@radix-ui/react-context-menu": "^2.1.5",
"body-scroll-lock": "^3.1.3",
"classnames": "^2.2.6",
"date-fns": "^2.16.1",
"debounce": "^1.2.1",
"github-slugger": "^1.3.0",
"next": "15.1.0",
Expand Down Expand Up @@ -62,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",
Expand Down
342 changes: 342 additions & 0 deletions scripts/deadLinkChecker.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading
Loading