Skip to content
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

Feat: error-decoder #6214

Merged
merged 19 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/MDX/ErrorCodesContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {createContext} from 'react';

export type ErrorCodes = Record<string, string> | null;

export const ErrorCodesContext = createContext<ErrorCodes>(null);
104 changes: 104 additions & 0 deletions src/components/MDX/ErrorDecoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {useContext, useMemo} from 'react';
import {useRouter} from 'next/router';
import {ErrorCodesContext} from './ErrorCodesContext';

function replaceArgs(msg: string, argList: string[]): string {
let argIdx = 0;
return msg.replace(/%s/g, function () {
const arg = argList[argIdx++];
return arg === undefined ? '[missing argument]' : arg;
});
}

// When the message contains a URL (like https://fb.me/react-refs-must-have-owner),
// make it a clickable link.
function urlify(str: string): React.ReactNode {
const urlRegex = /(https:\/\/fb\.me\/[a-z\-]+)/g;

const segments = str.split(urlRegex);

return segments.map((message, i) => {
if (i % 2 === 1) {
return (
<a key={i} target="_blank" rel="noopener noreferrer" href={message}>
{message}
</a>
);
}
return message;
});
}

// `?invariant=123&args[]=foo&args[]=bar`
// or `// ?invariant=123&args[0]=foo&args[1]=bar`
function parseQueryString(
search: string
): {code: string; args: string[]} | null {
const rawQueryString = search.substring(1);
if (!rawQueryString) {
return null;
}

let code = '';
let args = [];

const queries = rawQueryString.split('&');
for (let i = 0; i < queries.length; i++) {
const query = decodeURIComponent(queries[i]);
if (query.indexOf('invariant=') === 0) {
code = query.slice(10);
} else if (query.indexOf('args[') === 0) {
args.push(query.slice(query.indexOf(']=') + 2));
}
}

return {args, code};
}

function ErrorResult(props: {code?: string | null; msg: string}) {
if (!props.code) {
return (
<p>
When you encounter an error, you{"'"}ll receive a link to this page for
that specific error and we{"'"}ll show you the full error text.
</p>
);
}

return (
<div>
<p>
<b>The full text of the error you just encountered is:</b>
</p>
<code className="block bg-red-100 text-red-600 py-4 px-6 mt-5 rounded-lg">
<b>{urlify(props.msg)}</b>
</code>
</div>
);
}

export default function ErrorDecoder() {
const {isReady} = useRouter();
const errorCodes = useContext(ErrorCodesContext);

const [code, msg] = useMemo(() => {
let code = null;
let msg = '';

if (typeof window !== 'undefined' && isReady) {
const parseResult = parseQueryString(window.location.search);
SukkaW marked this conversation as resolved.
Show resolved Hide resolved
if (
parseResult != null &&
errorCodes != null &&
parseResult.code in errorCodes
) {
code = parseResult.code;
msg = replaceArgs(errorCodes[code], parseResult.args);
}
}

return [code, msg];
}, [errorCodes, isReady]);

return <ErrorResult code={code} msg={msg} />;
}
2 changes: 2 additions & 0 deletions src/components/MDX/MDXComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import ButtonLink from 'components/ButtonLink';
import {TocContext} from './TocContext';
import type {Toc, TocItem} from './TocContext';
import {TeamMember} from './TeamMember';
import ErrorDecoder from './ErrorDecoder';

function CodeStep({children, step}: {children: any; step: number}) {
return (
Expand Down Expand Up @@ -435,6 +436,7 @@ export const MDXComponents = {
Solution,
CodeStep,
YouTubeIframe,
ErrorDecoder,
};

for (let key in MDXComponents) {
Expand Down
10 changes: 10 additions & 0 deletions src/content/error-decoder/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
id: error-decoder
title: Error Decoder
---

In the minified production build of React, we avoid sending down full error messages in order to reduce the number of bytes sent over the wire.

We highly recommend using the development build locally when debugging your app since it tracks additional debug info and provides helpful warnings about potential problems in your apps, but if you encounter an exception while using the production build, this page will reassemble the original error message.

<ErrorDecoder />
2 changes: 1 addition & 1 deletion src/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ title: React
permalink: index.html
---

{/* See HomeContent.js */}
{/* See HomeContent.js */}
37 changes: 31 additions & 6 deletions src/pages/[[...markdownPath]].js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import {Fragment, useMemo} from 'react';
import {useRouter} from 'next/router';
import {MDXComponents} from 'components/MDX/MDXComponents';
import {ErrorCodesContext} from 'components/MDX/ErrorCodesContext.tsx';
import {Page} from 'components/Layout/Page';
import sidebarHome from '../sidebarHome.json';
import sidebarLearn from '../sidebarLearn.json';
import sidebarReference from '../sidebarReference.json';
import sidebarCommunity from '../sidebarCommunity.json';
import sidebarBlog from '../sidebarBlog.json';

export default function Layout({content, toc, meta}) {
export default function Layout({content, toc, meta, errorCodes}) {
const parsedContent = useMemo(
() => JSON.parse(content, reviveNodeOnClient),
[content]
Expand All @@ -39,9 +40,11 @@ export default function Layout({content, toc, meta}) {
break;
}
return (
<Page toc={parsedToc} routeTree={routeTree} meta={meta} section={section}>
{parsedContent}
</Page>
<ErrorCodesContext.Provider value={errorCodes || null}>
<Page toc={parsedToc} routeTree={routeTree} meta={meta} section={section}>
{parsedContent}
</Page>
</ErrorCodesContext.Provider>
);
}

Expand Down Expand Up @@ -111,6 +114,7 @@ export async function getStaticProps(context) {

// Read MDX from the file.
let path = (context.params.markdownPath || []).join('/') || 'index';

let mdx;
try {
mdx = fs.readFileSync(rootDir + path + '.md', 'utf8');
Expand Down Expand Up @@ -216,10 +220,31 @@ export async function getStaticProps(context) {
const fm = require('gray-matter');
const meta = fm(mdx).data;

// Serialize MDX into JSON.
rickhanlonii marked this conversation as resolved.
Show resolved Hide resolved
const content = JSON.stringify(children, stringifyNodeOnServer);
toc = JSON.stringify(toc, stringifyNodeOnServer);

// Do not cache /error-decoder pages in local filesystem, so we can have
// latest error codes from GitHub.
if (path === 'error-decoder') {
return {
props: {
content,
toc,
meta,
errorCodes: await (
await fetch(
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
)
).json(),
},
};
}
rickhanlonii marked this conversation as resolved.
Show resolved Hide resolved

const output = {
props: {
content: JSON.stringify(children, stringifyNodeOnServer),
toc: JSON.stringify(toc, stringifyNodeOnServer),
content,
toc,
meta,
},
};
Expand Down