Skip to content

Commit 3bc7c32

Browse files
committed
docs(en): merging all conflicts
2 parents b160d0d + d52b3ec commit 3bc7c32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+563
-20
lines changed

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
"prettier:diff": "yarn nit:source",
1616
"lint-heading-ids": "node scripts/headingIdLinter.js",
1717
"fix-headings": "node scripts/headingIdLinter.js --fix",
18-
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss",
18+
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss deadlinks",
1919
"tsc": "tsc --noEmit",
2020
"start": "next start",
2121
"postinstall": "is-ci || husky install .husky",
2222
"check-all": "npm-run-all prettier lint:fix tsc rss",
23-
"rss": "node scripts/generateRss.js"
23+
"rss": "node scripts/generateRss.js",
24+
"deadlinks": "node scripts/deadLinkChecker.js"
2425
},
2526
"dependencies": {
2627
"@codesandbox/sandpack-react": "2.13.5",
@@ -61,6 +62,7 @@
6162
"autoprefixer": "^10.4.2",
6263
"babel-eslint": "10.x",
6364
"babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112",
65+
"chalk": "4.1.2",
6466
"eslint": "7.x",
6567
"eslint-config-next": "12.0.3",
6668
"eslint-config-react-app": "^5.2.1",

scripts/deadLinkChecker.js

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const globby = require('globby');
6+
const chalk = require('chalk');
7+
8+
const CONTENT_DIR = path.join(__dirname, '../src/content');
9+
const PUBLIC_DIR = path.join(__dirname, '../public');
10+
const fileCache = new Map();
11+
const anchorMap = new Map(); // Map<filepath, Set<anchorId>>
12+
const contributorMap = new Map(); // Map<anchorId, URL>
13+
let errorCodes = new Set();
14+
15+
async function readFileWithCache(filePath) {
16+
if (!fileCache.has(filePath)) {
17+
try {
18+
const content = await fs.promises.readFile(filePath, 'utf8');
19+
fileCache.set(filePath, content);
20+
} catch (error) {
21+
throw new Error(`Failed to read file ${filePath}: ${error.message}`);
22+
}
23+
}
24+
return fileCache.get(filePath);
25+
}
26+
27+
async function fileExists(filePath) {
28+
try {
29+
await fs.promises.access(filePath, fs.constants.R_OK);
30+
return true;
31+
} catch {
32+
return false;
33+
}
34+
}
35+
36+
function getMarkdownFiles() {
37+
// Convert Windows paths to POSIX for globby compatibility
38+
const baseDir = CONTENT_DIR.replace(/\\/g, '/');
39+
const patterns = [
40+
path.posix.join(baseDir, '**/*.md'),
41+
path.posix.join(baseDir, '**/*.mdx'),
42+
];
43+
return globby.sync(patterns);
44+
}
45+
46+
function extractAnchorsFromContent(content) {
47+
const anchors = new Set();
48+
49+
// MDX-style heading IDs: {/*anchor-id*/}
50+
const mdxPattern = /\{\/\*([a-zA-Z0-9-_]+)\*\/\}/g;
51+
let match;
52+
while ((match = mdxPattern.exec(content)) !== null) {
53+
anchors.add(match[1].toLowerCase());
54+
}
55+
56+
// HTML id attributes
57+
const htmlIdPattern = /\sid=["']([a-zA-Z0-9-_]+)["']/g;
58+
while ((match = htmlIdPattern.exec(content)) !== null) {
59+
anchors.add(match[1].toLowerCase());
60+
}
61+
62+
// Markdown heading with explicit ID: ## Heading {#anchor-id}
63+
const markdownHeadingPattern = /^#+\s+.*\{#([a-zA-Z0-9-_]+)\}/gm;
64+
while ((match = markdownHeadingPattern.exec(content)) !== null) {
65+
anchors.add(match[1].toLowerCase());
66+
}
67+
68+
return anchors;
69+
}
70+
71+
async function buildAnchorMap(files) {
72+
for (const filePath of files) {
73+
const content = await readFileWithCache(filePath);
74+
const anchors = extractAnchorsFromContent(content);
75+
if (anchors.size > 0) {
76+
anchorMap.set(filePath, anchors);
77+
}
78+
}
79+
}
80+
81+
function extractLinksFromContent(content) {
82+
const linkPattern = /\[([^\]]*)\]\(([^)]+)\)/g;
83+
const links = [];
84+
let match;
85+
86+
while ((match = linkPattern.exec(content)) !== null) {
87+
const [, linkText, linkUrl] = match;
88+
if (linkUrl.startsWith('/') && !linkUrl.startsWith('//')) {
89+
const lines = content.substring(0, match.index).split('\n');
90+
const line = lines.length;
91+
const lastLineStart =
92+
lines.length > 1 ? content.lastIndexOf('\n', match.index - 1) + 1 : 0;
93+
const column = match.index - lastLineStart + 1;
94+
95+
links.push({
96+
text: linkText,
97+
url: linkUrl,
98+
line,
99+
column,
100+
});
101+
}
102+
}
103+
104+
return links;
105+
}
106+
107+
async function findTargetFile(urlPath) {
108+
// Check if it's an image or static asset that might be in the public directory
109+
const imageExtensions = [
110+
'.png',
111+
'.jpg',
112+
'.jpeg',
113+
'.gif',
114+
'.svg',
115+
'.ico',
116+
'.webp',
117+
];
118+
const hasImageExtension = imageExtensions.some((ext) =>
119+
urlPath.toLowerCase().endsWith(ext)
120+
);
121+
122+
if (hasImageExtension || urlPath.includes('.')) {
123+
// Check in public directory (with and without leading slash)
124+
const publicPaths = [
125+
path.join(PUBLIC_DIR, urlPath),
126+
path.join(PUBLIC_DIR, urlPath.substring(1)),
127+
];
128+
129+
for (const p of publicPaths) {
130+
if (await fileExists(p)) {
131+
return p;
132+
}
133+
}
134+
}
135+
136+
const possiblePaths = [
137+
path.join(CONTENT_DIR, urlPath + '.md'),
138+
path.join(CONTENT_DIR, urlPath + '.mdx'),
139+
path.join(CONTENT_DIR, urlPath, 'index.md'),
140+
path.join(CONTENT_DIR, urlPath, 'index.mdx'),
141+
// Without leading slash
142+
path.join(CONTENT_DIR, urlPath.substring(1) + '.md'),
143+
path.join(CONTENT_DIR, urlPath.substring(1) + '.mdx'),
144+
path.join(CONTENT_DIR, urlPath.substring(1), 'index.md'),
145+
path.join(CONTENT_DIR, urlPath.substring(1), 'index.mdx'),
146+
];
147+
148+
for (const p of possiblePaths) {
149+
if (await fileExists(p)) {
150+
return p;
151+
}
152+
}
153+
return null;
154+
}
155+
156+
async function validateLink(link) {
157+
const urlAnchorPattern = /#([a-zA-Z0-9-_]+)$/;
158+
const anchorMatch = link.url.match(urlAnchorPattern);
159+
const urlWithoutAnchor = link.url.replace(urlAnchorPattern, '');
160+
161+
if (urlWithoutAnchor === '/') {
162+
return {valid: true};
163+
}
164+
165+
// Check if it's an error code link
166+
const errorCodeMatch = urlWithoutAnchor.match(/^\/errors\/(\d+)$/);
167+
if (errorCodeMatch) {
168+
const code = errorCodeMatch[1];
169+
if (!errorCodes.has(code)) {
170+
return {
171+
valid: false,
172+
reason: `Error code ${code} not found in React error codes`,
173+
};
174+
}
175+
return {valid: true};
176+
}
177+
178+
// Check if it's a contributor link on the team or acknowledgements page
179+
if (
180+
anchorMatch &&
181+
(urlWithoutAnchor === '/community/team' ||
182+
urlWithoutAnchor === '/community/acknowledgements')
183+
) {
184+
const anchorId = anchorMatch[1].toLowerCase();
185+
if (contributorMap.has(anchorId)) {
186+
const correctUrl = contributorMap.get(anchorId);
187+
if (correctUrl !== link.url) {
188+
return {
189+
valid: false,
190+
reason: `Contributor link should be updated to: ${correctUrl}`,
191+
};
192+
}
193+
return {valid: true};
194+
} else {
195+
return {
196+
valid: false,
197+
reason: `Contributor link not found`,
198+
};
199+
}
200+
}
201+
202+
const targetFile = await findTargetFile(urlWithoutAnchor);
203+
204+
if (!targetFile) {
205+
return {
206+
valid: false,
207+
reason: `Target file not found for: ${urlWithoutAnchor}`,
208+
};
209+
}
210+
211+
// Only check anchors for content files, not static assets
212+
if (anchorMatch && targetFile.startsWith(CONTENT_DIR)) {
213+
const anchorId = anchorMatch[1].toLowerCase();
214+
215+
// TODO handle more special cases. These are usually from custom MDX components that include
216+
// a Heading from src/components/MDX/Heading.tsx which automatically injects an anchor tag.
217+
switch (anchorId) {
218+
case 'challenges':
219+
case 'recap': {
220+
return {valid: true};
221+
}
222+
}
223+
224+
const fileAnchors = anchorMap.get(targetFile);
225+
226+
if (!fileAnchors || !fileAnchors.has(anchorId)) {
227+
return {
228+
valid: false,
229+
reason: `Anchor #${anchorMatch[1]} not found in ${path.relative(
230+
CONTENT_DIR,
231+
targetFile
232+
)}`,
233+
};
234+
}
235+
}
236+
237+
return {valid: true};
238+
}
239+
240+
async function processFile(filePath) {
241+
const content = await readFileWithCache(filePath);
242+
const links = extractLinksFromContent(content);
243+
const deadLinks = [];
244+
245+
for (const link of links) {
246+
const result = await validateLink(link);
247+
if (!result.valid) {
248+
deadLinks.push({
249+
file: path.relative(process.cwd(), filePath),
250+
line: link.line,
251+
column: link.column,
252+
text: link.text,
253+
url: link.url,
254+
reason: result.reason,
255+
});
256+
}
257+
}
258+
259+
return {deadLinks, totalLinks: links.length};
260+
}
261+
262+
async function buildContributorMap() {
263+
const teamFile = path.join(CONTENT_DIR, 'community/team.md');
264+
const teamContent = await readFileWithCache(teamFile);
265+
266+
const teamMemberPattern = /<TeamMember[^>]*permalink=["']([^"']+)["']/g;
267+
let match;
268+
269+
while ((match = teamMemberPattern.exec(teamContent)) !== null) {
270+
const permalink = match[1];
271+
contributorMap.set(permalink, `/community/team#${permalink}`);
272+
}
273+
274+
const ackFile = path.join(CONTENT_DIR, 'community/acknowledgements.md');
275+
const ackContent = await readFileWithCache(ackFile);
276+
const contributorPattern = /\*\s*\[([^\]]+)\]\(([^)]+)\)/g;
277+
278+
while ((match = contributorPattern.exec(ackContent)) !== null) {
279+
const name = match[1];
280+
const url = match[2];
281+
const hyphenatedName = name.toLowerCase().replace(/\s+/g, '-');
282+
if (!contributorMap.has(hyphenatedName)) {
283+
contributorMap.set(hyphenatedName, url);
284+
}
285+
}
286+
}
287+
288+
async function fetchErrorCodes() {
289+
try {
290+
const response = await fetch(
291+
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
292+
);
293+
if (!response.ok) {
294+
throw new Error(`Failed to fetch error codes: ${response.status}`);
295+
}
296+
const codes = await response.json();
297+
errorCodes = new Set(Object.keys(codes));
298+
console.log(chalk.gray(`Fetched ${errorCodes.size} React error codes\n`));
299+
} catch (error) {
300+
throw new Error(`Failed to fetch error codes: ${error.message}`);
301+
}
302+
}
303+
304+
async function main() {
305+
const files = getMarkdownFiles();
306+
console.log(chalk.gray(`Checking ${files.length} markdown files...`));
307+
308+
await fetchErrorCodes();
309+
await buildContributorMap();
310+
await buildAnchorMap(files);
311+
312+
const filePromises = files.map((filePath) => processFile(filePath));
313+
const results = await Promise.all(filePromises);
314+
const deadLinks = results.flatMap((r) => r.deadLinks);
315+
const totalLinks = results.reduce((sum, r) => sum + r.totalLinks, 0);
316+
317+
if (deadLinks.length > 0) {
318+
for (const link of deadLinks) {
319+
console.log(chalk.yellow(`${link.file}:${link.line}:${link.column}`));
320+
console.log(chalk.reset(` Link text: ${link.text}`));
321+
console.log(chalk.reset(` URL: ${link.url}`));
322+
console.log(` ${chalk.red('✗')} ${chalk.red(link.reason)}\n`);
323+
}
324+
325+
console.log(
326+
chalk.red(
327+
`\nFound ${deadLinks.length} dead link${
328+
deadLinks.length > 1 ? 's' : ''
329+
} out of ${totalLinks} total links\n`
330+
)
331+
);
332+
process.exit(1);
333+
}
334+
335+
console.log(chalk.green(`\n✓ All ${totalLinks} links are valid!\n`));
336+
process.exit(0);
337+
}
338+
339+
main().catch((error) => {
340+
console.log(chalk.red(`Error: ${error.message}`));
341+
process.exit(1);
342+
});

src/content/blog/2023/03/16/introducing-react-dev.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,13 @@ description: 今天,我们非常高兴推出 react.dev,React 及其文档的
4141

4242
**新文档从一开始就使用 Hook 来介绍 React**。新文档分为两个主要部分:
4343

44+
<<<<<<< HEAD
4445
* **[学习 React](/learn)** 是一个自学课程,从头开始介绍 React。
4546
* **[API 参考](/reference)** 提供了每个 React API 的详细信息和使用示例。
47+
=======
48+
* **[Learn React](/learn)** is a self-paced course that teaches React from scratch.
49+
* **[API Reference](/reference/react)** provides the details and usage examples for every React API.
50+
>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73
4651
4752
让我们更仔细地看看可以从每个部分中找到什么。
4853

@@ -607,7 +612,11 @@ button { display: block; margin-top: 10px; }
607612

608613
</Recipes>
609614

615+
<<<<<<< HEAD
610616
一些 API 页面还包括针对常见问题的 [故障排除](/reference/react/useEffect#troubleshooting) 和针对弃用 API 的 [替代方案](/reference/react-dom/findDOMNode#alternatives)
617+
=======
618+
Some API pages also include [Troubleshooting](/reference/react/useEffect#troubleshooting) (for common problems) and [Alternatives](https://18.react.dev/reference/react-dom/findDOMNode#alternatives) (for deprecated APIs).
619+
>>>>>>> d52b3ec734077fd56f012fc2b30a67928d14cc73
611620
612621
我们希望 API 参考不仅仅是用来查找参数,还可以用来查看任何给定 API 可以做的所有不同事情以及与其他 API 相关联的方法。
613622

0 commit comments

Comments
 (0)