Skip to content

Commit a01b0cc

Browse files
merging all conflicts
2 parents 24ef03b + d52b3ec commit a01b0cc

48 files changed

Lines changed: 553 additions & 26 deletions

Some content is hidden

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

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 @@ Quando lançamos o React Hooks em 2018, os documentos do Hooks assumiram que o l
4141

4242
**A nova documentação ensina React com Hooks desde o início.** A documentação está dividida em duas seções principais:
4343

44+
<<<<<<< HEAD
4445
* **[Aprenda React](/learn)** é um curso individualizado que ensina React do zero.
4546
* **[Referências de API](/reference)** fornece os detalhes e exemplos de uso para cada API React.
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
Vamos ver mais detalhadamente o que pode encontrar em cada seção.
4853

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

608613
</Recipes>
609614

615+
<<<<<<< HEAD
610616
Algumas páginas da API também incluem [Solução de problemas](/reference/react/useEffect#troubleshooting) (para problemas comuns) e [Alternativas](/reference/react-dom/findDOMNode#alternatives) (para APIs obsoletas).
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
Esperamos que esta abordagem torne a referência da API útil não só como uma forma de procurar um argumento, mas também como uma forma de ver todas as coisas diferentes que pode fazer com uma determinada API - e como esta se liga às outras.
613622

0 commit comments

Comments
 (0)