Revert "feat: migrate React.dev to the App Router (#7437)" (#7466)

This reverts commit 0eb0f889b1.
This commit is contained in:
dan
2025-02-01 19:21:43 +00:00
committed by GitHub
parent 0eb0f889b1
commit af0358f1d2
70 changed files with 1628 additions and 1798 deletions

View File

@@ -8,9 +8,7 @@
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"react-hooks/exhaustive-deps": "error",
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
"react-compiler/react-compiler": "error",
"@next/next/no-img-element": "off",
"@next/next/no-html-link-for-pages": "off"
"react-compiler/react-compiler": "error"
},
"env": {
"node": true,

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View File

@@ -11,11 +11,8 @@ const nextConfig = {
experimental: {
scrollRestoration: true,
reactCompiler: true,
newDevOverlay: true,
},
env: {},
serverExternalPackages: [],
webpack: (config, {dev, isServer, ...options}) => {
if (process.env.ANALYZE) {
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');

View File

@@ -5,7 +5,7 @@
"license": "CC",
"scripts": {
"analyze": "ANALYZE=true next build",
"dev": "next dev",
"dev": "next-remote-watch ./src/content",
"build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs",
"lint": "next lint",
"lint:fix": "next lint --fix",
@@ -15,11 +15,12 @@
"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",
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss",
"tsc": "tsc --noEmit",
"start": "next start",
"postinstall": "is-ci || husky install .husky",
"check-all": "npm-run-all prettier lint:fix tsc"
"check-all": "npm-run-all prettier lint:fix tsc rss",
"rss": "node scripts/generateRss.js"
},
"dependencies": {
"@codesandbox/sandpack-react": "2.13.5",
@@ -27,21 +28,19 @@
"@docsearch/react": "^3.8.3",
"@headlessui/react": "^1.7.0",
"@radix-ui/react-context-menu": "^2.1.5",
"@types/mdast": "^4.0.4",
"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.2.0-canary.33",
"next": "15.1.0",
"next-remote-watch": "^1.0.0",
"parse-numeric-range": "^1.2.0",
"react": "^19.0.0",
"react-collapsed": "4.0.4",
"react-dom": "^19.0.0",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"unist-builder": "^4.0.0"
"remark-gfm": "^3.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.9",
@@ -63,7 +62,6 @@
"autoprefixer": "^10.4.2",
"babel-eslint": "10.x",
"babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112",
"chokidar": "^4.0.3",
"eslint": "7.x",
"eslint-config-next": "12.0.3",
"eslint-config-react-app": "^5.2.1",

6
scripts/generateRss.js Normal file
View File

@@ -0,0 +1,6 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
const {generateRssFeed} = require('../src/utils/rss');
generateRssFeed();

View File

@@ -1,172 +0,0 @@
import fs from 'fs/promises';
import path from 'path';
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';
import {generateMDX} from '../../utils/generateMDX';
import {generateMetadata as generateSeoMetadata} from '../../utils/generateMetadata';
import {getRouteMeta, RouteItem} from 'components/Layout/getRouteMeta';
import {LanguageItem} from 'components/MDX/LanguagesContext';
import {cache} from 'react';
function getActiveSection(pathname: string) {
if (pathname === '/') {
return 'home';
} else if (pathname.startsWith('/reference')) {
return 'reference';
} else if (pathname.startsWith('/learn')) {
return 'learn';
} else if (pathname.startsWith('/community')) {
return 'community';
} else if (pathname.startsWith('/blog')) {
return 'blog';
} else {
return 'unknown';
}
}
async function getRouteTree(section: string) {
switch (section) {
case 'home':
case 'unknown':
return sidebarHome;
case 'learn':
return sidebarLearn;
case 'reference':
return sidebarReference;
case 'community':
return sidebarCommunity;
case 'blog':
return sidebarBlog;
default:
throw new Error(`Unknown section: ${section}`);
}
}
const getPageContent = cache(async function getPageContent(
markdownPath: any[]
) {
const rootDir = path.join(process.cwd(), 'src/content');
let mdxPath = markdownPath?.join('/') || 'index';
let mdx;
try {
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
} catch {
mdx = await fs.readFile(path.join(rootDir, mdxPath, 'index.md'), 'utf8');
}
return await generateMDX(mdx, mdxPath, {});
});
// This replaces getStaticPaths
export async function generateStaticParams() {
const rootDir = path.join(process.cwd(), 'src/content');
async function getFiles(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, {withFileTypes: true});
const files = await Promise.all(
entries.map(async (entry) => {
const res = path.resolve(dir, entry.name);
return entry.isDirectory()
? getFiles(res)
: res.slice(rootDir.length + 1);
})
);
return files
.flat()
.filter(
(file: string) => file.endsWith('.md') && !file.startsWith('errors/')
);
}
function getSegments(file: string) {
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
if (segments[segments.length - 1] === 'index') {
segments.pop();
}
return segments;
}
const files = await getFiles(rootDir);
return files.map((file: any) => ({
markdownPath: getSegments(file),
}));
}
export default async function WrapperPage({
params,
}: {
params: Promise<{markdownPath: string[]}>;
}) {
const {markdownPath} = await params;
// Get the MDX content and associated data
const {content, toc, meta} = await getPageContent(markdownPath);
const pathname = '/' + (markdownPath?.join('/') || '');
const section = getActiveSection(pathname);
const routeTree = await getRouteTree(section);
// Load the list of translated languages conditionally.
let languages: Array<LanguageItem> | null = null;
if (pathname.endsWith('/translations')) {
languages = await (
await fetch(
'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'
)
).json(); // { code: string; name: string; enName: string}[]
}
// Pass the content and TOC directly, as `getPageContent` should already return them in the correct format
return (
<Page
toc={toc} // Pass the TOC directly without parsing
routeTree={routeTree as RouteItem}
meta={meta}
section={section}
pathname={pathname}
languages={languages}>
{content}
</Page>
);
}
// Configure dynamic segments to be statically generated
export const dynamicParams = false;
export async function generateMetadata({
params,
}: {
params: Promise<{markdownPath: string[]}>;
}) {
const {markdownPath} = await params;
const pathname = '/' + (markdownPath?.join('/') || '');
const section = getActiveSection(pathname);
const routeTree = await getRouteTree(section);
const {route, order} = getRouteMeta(pathname, routeTree as RouteItem);
const {
title = route?.title || '',
description = route?.description || '',
titleForTitleTag,
} = await getPageContent(markdownPath).then(({meta}) => meta);
return generateSeoMetadata({
title,
isHomePage: pathname === '/',
path: pathname,
description,
titleForTitleTag,
image: `/images/og-${section}.png`,
searchOrder:
section === 'learn' || (section === 'blog' && pathname !== '/blog')
? order
: undefined,
});
}

View File

@@ -1,118 +0,0 @@
import {Page} from 'components/Layout/Page';
import sidebarLearn from '../../../sidebarLearn.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {generateMDX} from 'utils/generateMDX';
import fs from 'fs/promises';
import path from 'path';
import {ErrorDecoderProvider} from 'components/ErrorDecoderProvider';
import {notFound} from 'next/navigation';
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
let errorCodesCache: Record<string, string> | null = null;
async function getErrorCodes() {
if (!errorCodesCache) {
const response = await fetch(
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
);
errorCodesCache = await response.json();
}
return errorCodesCache;
}
export async function generateStaticParams() {
const errorCodes = await getErrorCodes();
const staticParams = Object.keys(errorCodes!).map((code) => ({
errorCode: [code],
})) as Array<{errorCode: string[] | undefined}>;
staticParams.push({errorCode: undefined});
return staticParams;
}
async function getErrorPageContent(params: {errorCode: string[]}) {
if (params.errorCode?.length > 1) {
notFound();
}
const code = params.errorCode?.[0];
const errorCodes = await getErrorCodes();
if (code && !errorCodes?.[code]) {
notFound();
}
const rootDir = path.join(process.cwd(), 'src/content/errors');
let mdxPath = params?.errorCode || 'index';
let mdx;
try {
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
} catch {
mdx = await fs.readFile(path.join(rootDir, 'generic.md'), 'utf8');
}
const {content, toc, meta} = await generateMDX(mdx, mdxPath, {
code,
errorCodes,
});
return {
content,
toc,
meta,
errorCode: code,
errorMessage: code ? errorCodes![code] : null,
};
}
export default async function ErrorDecoderPage({
params,
}: {
params: Promise<{errorCode: string[]}>;
}) {
const {content, errorMessage, errorCode} = await getErrorPageContent(
await params
);
return (
<ErrorDecoderProvider errorMessage={errorMessage} errorCode={errorCode}>
<Page
pathname={`/errors/${errorCode}`}
toc={[]}
meta={{
title: errorCode
? `Minified React error #${errorCode}`
: 'Minified Error Decoder',
}}
routeTree={sidebarLearn as RouteItem}
section="unknown">
<div className="whitespace-pre-line">{content}</div>
</Page>
</ErrorDecoderProvider>
);
}
// Disable dynamic params to ensure all pages are statically generated
export const dynamicParams = false;
export async function generateMetadata({
params,
}: {
params: Promise<{errorCode: string[]}>;
}) {
const {errorCode} = await params;
const title = errorCode
? `Minified React error #${errorCode}`
: 'Minified Error Decoder';
return generateSeoMetadata({
title,
path: `errors/${errorCode}`,
isHomePage: false,
});
}

View File

@@ -1,157 +0,0 @@
import {siteConfig} from '../siteConfig';
import {Analytics} from 'components/Analytics';
import {ScrollHandler} from 'components/SafariScrollHandler';
import '@docsearch/css';
import '../styles/algolia.css';
import '../styles/index.css';
import '../styles/sandpack.css';
import {Suspense} from 'react';
import {DevContentRefresher} from 'components/DevContentRefresher';
import {ThemeScript} from 'components/ThemeScript';
import {UwuScript} from 'components/UwuScript';
import {Metadata} from 'next';
export const viewport = {
themeColor: [
{media: '(prefers-color-scheme: light)', color: '#23272f'},
{media: '(prefers-color-scheme: dark)', color: '#23272f'},
],
};
export const metadata: Metadata = {
description:
'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript.',
openGraph: {
siteName: 'React',
type: 'website',
images: [{url: '/images/og-default.png'}],
},
twitter: {
card: 'summary_large_image',
site: '@reactjs',
creator: '@reactjs',
images: ['/images/og-default.png'],
},
verification: {
google: 'sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0',
},
other: {
'msapplication-TileColor': '#2b5797',
'fb:app_id': '623268441017527',
},
icons: {
icon: [
{url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png'},
{url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png'},
],
apple: [
{url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png'},
],
other: [
{rel: 'mask-icon', url: '/safari-pinned-tab.svg', color: '#404756'},
],
},
manifest: '/site.webmanifest',
};
function FontPreload() {
return (
<>
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Md.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_SBd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_MdIt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Text_W_BdIt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Bd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Text_W_Md.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Text_W_Bd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Text_W_Rg.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Text_W_It.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</>
);
}
export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html
lang={siteConfig.languageCode}
dir={siteConfig.isRTL ? 'rtl' : 'ltr'}
suppressHydrationWarning>
<head>
<FontPreload />
</head>
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
<ScrollHandler />
<ThemeScript />
<UwuScript />
{process.env.NODE_ENV !== 'production' && <DevContentRefresher />}
{children}
<Suspense fallback={null}>
<Analytics />
</Suspense>
</body>
</html>
);
}

View File

@@ -1,69 +0,0 @@
import Feed from 'rss';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const getAllFiles = (dirPath, arrayOfFiles = []) => {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
arrayOfFiles.push(filePath);
}
});
return arrayOfFiles;
};
export async function GET() {
const feed = new Feed({
title: 'React Blog',
description:
'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.',
feed_url: 'https://react.dev/rss.xml',
site_url: 'https://react.dev/',
language: 'en',
favicon: 'https://react.dev/favicon.ico',
pubDate: new Date(),
generator: 'react.dev rss module',
});
const dirPath = path.join(process.cwd(), 'src/content/blog');
const filesByOldest = getAllFiles(dirPath);
const files = filesByOldest.reverse();
for (const filePath of files) {
const id = path.basename(filePath);
if (id !== 'index.md') {
const content = fs.readFileSync(filePath, 'utf-8');
const {data} = matter(content);
const slug = filePath.split('/').slice(-4).join('/').replace('.md', '');
if (!data.title || !data.author || !data.date || !data.description) {
throw new Error(
`${id}: Blog posts must include title, author, date, and description in metadata.`
);
}
feed.item({
id,
title: data.title,
author: data.author,
date: new Date(data.date),
url: `https://react.dev/blog/${slug}`,
description: data.description,
});
}
}
return new Response(feed.xml({indent: true}), {
headers: {
'Content-Type': 'application/rss+xml',
},
});
}
export const dynamic = 'force-static';

View File

@@ -1,63 +0,0 @@
'use client';
import {useEffect} from 'react';
import Script from 'next/script';
declare global {
interface Window {
gtag: (...args: any[]) => void;
}
}
export function Analytics() {
useEffect(() => {
if (typeof window !== 'undefined') {
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
const handleTermination = () => {
window.gtag?.('event', 'timing', {
event_label: 'JS Dependencies',
event: 'unload',
});
};
window.addEventListener(terminationEvent, handleTermination);
return () =>
window.removeEventListener(terminationEvent, handleTermination);
}
}, []);
useEffect(() => {
// If only we had router events. But patching pushState is what Vercel Analytics does.
// https://va.vercel-scripts.com/v1/script.debug.js
const originalPushState = history.pushState;
history.pushState = function (...args) {
const oldCleanedUrl = window.location.href.split(/[\?\#]/)[0];
originalPushState.apply(history, args);
const newCleanedUrl = window.location.href.split(/[\?\#]/)[0];
if (oldCleanedUrl !== newCleanedUrl) {
window?.gtag('set', 'page', newCleanedUrl);
window?.gtag('send', 'pageview');
}
};
return () => {
history.pushState = originalPushState;
};
}, []);
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_TRACKING_ID}');
`}
</Script>
</>
);
}

View File

@@ -1,29 +0,0 @@
'use client';
import {useRouter} from 'next/navigation';
import {useRef, useEffect} from 'react';
export function DevContentRefresher() {
const router = useRouter();
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
wsRef.current = new WebSocket('ws://localhost:3001');
wsRef.current.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.event === 'refresh') {
console.log('Refreshing content...');
// @ts-ignore
router.hmrRefresh(); // Triggers client-side refresh
}
};
return () => {
wsRef.current?.close();
};
}, [router]);
return null;
}

View File

@@ -1,19 +0,0 @@
'use client';
import {ErrorDecoderContext} from './ErrorDecoderContext';
export function ErrorDecoderProvider({
children,
errorMessage,
errorCode,
}: {
children: React.ReactNode;
errorMessage: string | null;
errorCode: string | null;
}) {
return (
<ErrorDecoderContext.Provider value={{errorMessage, errorCode}}>
{children}
</ErrorDecoderContext.Provider>
);
}

View File

@@ -3,12 +3,12 @@
*/
import {useState} from 'react';
import {useRouter} from 'next/router';
import cn from 'classnames';
import {usePathname} from 'next/navigation';
export function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) {
const pathname = usePathname();
const cleanedPath = pathname.split(/[\?\#]/)[0];
const {asPath} = useRouter();
const cleanedPath = asPath.split(/[\?\#]/)[0];
// Reset on route changes.
return <SendFeedback key={cleanedPath} onSubmit={onSubmit} />;
}

View File

@@ -1,16 +1,16 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import * as React from 'react';
import {Suspense} from 'react';
import * as React from 'react';
import {useRouter} from 'next/router';
import {SidebarNav} from './SidebarNav';
import {Footer} from './Footer';
import {Toc} from './Toc';
// import SocialBanner from '../SocialBanner';
import {DocsPageFooter} from 'components/DocsFooter';
import {Seo} from 'components/Seo';
import PageHeading from 'components/PageHeading';
import {getRouteMeta} from './getRouteMeta';
import {TocContext} from '../MDX/TocContext';
@@ -20,8 +20,8 @@ import type {RouteItem} from 'components/Layout/getRouteMeta';
import {HomeContent} from './HomeContent';
import {TopNav} from './TopNav';
import cn from 'classnames';
import Head from 'next/head';
// Prefetch the code block component
import(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock');
interface PageProps {
@@ -36,7 +36,6 @@ interface PageProps {
};
section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown';
languages?: Languages | null;
pathname: string;
}
export function Page({
@@ -45,11 +44,11 @@ export function Page({
routeTree,
meta,
section,
pathname,
languages = null,
}: PageProps) {
const cleanedPath = pathname.split(/[\?\#]/)[0];
const {route, nextRoute, prevRoute, breadcrumbs} = getRouteMeta(
const {asPath} = useRouter();
const cleanedPath = asPath.split(/[\?\#]/)[0];
const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta(
cleanedPath,
routeTree
);
@@ -114,17 +113,31 @@ export function Page({
showSidebar = false;
}
let searchOrder;
if (section === 'learn' || (section === 'blog' && !isBlogIndex)) {
searchOrder = order;
}
return (
<>
<Seo
title={title}
titleForTitleTag={meta.titleForTitleTag}
isHomePage={isHomePage}
image={`/images/og-` + section + '.png'}
searchOrder={searchOrder}
/>
{(isHomePage || isBlogIndex) && (
// RSS Feed link is now handled by metadata in layout.tsx
<link
rel="alternate"
type="application/rss+xml"
title="React Blog RSS Feed"
href="/rss.xml"
/>
<Head>
<link
rel="alternate"
type="application/rss+xml"
title="React Blog RSS Feed"
href="/rss.xml"
/>
</Head>
)}
{/*<SocialBanner />*/}
<TopNav
section={section}
routeTree={routeTree}
@@ -149,7 +162,9 @@ export function Page({
{/* No fallback UI so need to be careful not to suspend directly inside. */}
<Suspense fallback={null}>
<main className="min-w-0 isolate">
<article className="font-normal break-words text-primary dark:text-primary-dark">
<article
className="font-normal break-words text-primary dark:text-primary-dark"
key={asPath}>
{content}
</article>
<div
@@ -173,7 +188,7 @@ export function Page({
</main>
</Suspense>
<div className="hidden -mt-16 lg:max-w-custom-xs 2xl:block">
{showToc && toc.length > 0 && <Toc headings={toc} key={pathname} />}
{showToc && toc.length > 0 && <Toc headings={toc} key={asPath} />}
</div>
</div>
</>

View File

@@ -5,12 +5,12 @@
import {useRef, useLayoutEffect, Fragment} from 'react';
import cn from 'classnames';
import {useRouter} from 'next/router';
import {SidebarLink} from './SidebarLink';
import {useCollapse} from 'react-collapsed';
import usePendingRoute from 'hooks/usePendingRoute';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {siteConfig} from 'siteConfig';
import {usePathname} from 'next/navigation';
interface SidebarRouteTreeProps {
isForceExpanded: boolean;
@@ -77,7 +77,7 @@ export function SidebarRouteTree({
routeTree,
level = 0,
}: SidebarRouteTreeProps) {
const slug = usePathname().split(/[\?\#]/)[0];
const slug = useRouter().asPath.split(/[\?\#]/)[0];
const pendingRoute = usePendingRoute();
const currentRoutes = routeTree.routes as RouteItem[];
return (

View File

@@ -11,11 +11,7 @@ export function Toc({headings}: {headings: Toc}) {
// TODO: We currently have a mismatch between the headings in the document
// and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges).
// Select the max TOC item we have here for now, but remove this after the fix.
const selectedIndex =
currentIndex !== undefined
? Math.min(currentIndex, headings.length - 1)
: -1;
const selectedIndex = Math.min(currentIndex, headings.length - 1);
return (
<nav role="navigation" className="pt-20 sticky top-0 end-0">
{headings.length > 0 && (
@@ -55,7 +51,7 @@ export function Toc({headings}: {headings: Toc}) {
'block hover:text-link dark:hover:text-link-dark leading-normal py-2'
)}
href={h.url}>
{h.node}
{h.text}
</a>
</li>
);

View File

@@ -14,6 +14,7 @@ import Image from 'next/image';
import * as React from 'react';
import cn from 'classnames';
import NextLink from 'next/link';
import {useRouter} from 'next/router';
import {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock';
import {IconClose} from 'components/Icon/IconClose';
@@ -26,7 +27,6 @@ import {SidebarRouteTree} from '../Sidebar';
import type {RouteItem} from '../getRouteMeta';
import {siteConfig} from 'siteConfig';
import BrandMenu from './BrandMenu';
import {usePathname} from 'next/navigation';
declare global {
interface Window {
@@ -162,7 +162,7 @@ export default function TopNav({
const [showSearch, setShowSearch] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const scrollParentRef = useRef<HTMLDivElement>(null);
const pathname = usePathname();
const {asPath} = useRouter();
// HACK. Fix up the data structures instead.
if ((routeTree as any).routes.length === 1) {
@@ -183,7 +183,7 @@ export default function TopNav({
// Close the overlay on any navigation.
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);
}, [asPath]);
// Also close the overlay if the window gets resized past mobile layout.
// (This is also important because we don't want to keep the body locked!)

View File

@@ -23,10 +23,7 @@ export function getHeaderAnchors(): HTMLAnchorElement[] {
* Sets up Table of Contents highlighting.
*/
export function useTocHighlight() {
const [currentIndex, setCurrentIndex] = useState<number | undefined>(
undefined
);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const timeoutRef = useRef<number | null>(null);
useEffect(() => {

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
@@ -11,7 +9,7 @@ import {H2} from 'components/MDX/Heading';
import {H4} from 'components/MDX/Heading';
import {Challenge} from './Challenge';
import {Navigation} from './Navigation';
import {usePathname} from 'next/navigation';
import {useRouter} from 'next/router';
interface ChallengesProps {
children: React.ReactElement[];
@@ -42,13 +40,11 @@ const parseChallengeContents = (
let challenge: Partial<ChallengeContents> = {};
let content: React.ReactElement[] = [];
Children.forEach(children, (child) => {
const {props} = child as React.ReactElement<{
const {props, type} = child as React.ReactElement<{
children?: string;
id?: string;
'data-mdx-name'?: string;
}>;
switch (props?.['data-mdx-name']) {
switch ((type as any).mdxName) {
case 'Solution': {
challenge.solution = child;
challenge.content = content;
@@ -94,12 +90,12 @@ export function Challenges({
const queuedScrollRef = useRef<undefined | QueuedScroll>(QueuedScroll.INIT);
const [activeIndex, setActiveIndex] = useState(0);
const currentChallenge = challenges[activeIndex];
const pathname = usePathname();
const {asPath} = useRouter();
useEffect(() => {
if (queuedScrollRef.current === QueuedScroll.INIT) {
const initIndex = challenges.findIndex(
(challenge) => challenge.id === pathname.split('#')[1]
(challenge) => challenge.id === asPath.split('#')[1]
);
if (initIndex === -1) {
queuedScrollRef.current = undefined;
@@ -116,7 +112,7 @@ export function Challenges({
});
queuedScrollRef.current = undefined;
}
}, [activeIndex, pathname, challenges]);
}, [activeIndex, asPath, challenges]);
const handleChallengeChange = (index: number) => {
setActiveIndex(index);

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -16,7 +16,7 @@ export function CodeDiagram({children, flip = false}: CodeDiagramProps) {
return child.type === 'img';
});
const content = Children.toArray(children).map((child: any) => {
if (child.props?.['data-mdx-name'] === 'pre') {
if (child.type?.mdxName === 'pre') {
return (
<CodeBlock
key={child.key}

View File

@@ -1,5 +1,3 @@
'use client';
import {useEffect, useState} from 'react';
import {useErrorDecoderParams} from '../ErrorDecoderContext';
import cn from 'classnames';

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
@@ -11,8 +9,8 @@ import {IconDeepDive} from '../Icon/IconDeepDive';
import {IconCodeBlock} from '../Icon/IconCodeBlock';
import {Button} from '../Button';
import {H4} from './Heading';
import {useRouter} from 'next/router';
import {useEffect, useRef, useState} from 'react';
import {usePathname} from 'next/navigation';
interface ExpandableExampleProps {
children: React.ReactNode;
@@ -21,23 +19,17 @@ interface ExpandableExampleProps {
}
function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
// Validate children using `data-mdx-name`
if (
!Array.isArray(children) ||
children[0].props?.['data-mdx-name'] !== 'h4'
) {
if (!Array.isArray(children) || children[0].type.mdxName !== 'h4') {
throw Error(
`Expandable content ${type} is missing a corresponding title at the beginning`
);
}
const isDeepDive = type === 'DeepDive';
const isExample = type === 'Example';
const id = children[0].props.id;
const pathname = usePathname();
const shouldAutoExpand = id === pathname.split('#')[1];
const {asPath} = useRouter();
const shouldAutoExpand = id === asPath.split('#')[1];
const queuedExpandRef = useRef<boolean>(shouldAutoExpand);
const [isExpanded, setIsExpanded] = useState(false);
@@ -65,7 +57,8 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
className="list-none p-8"
tabIndex={-1 /* there's a button instead */}
onClick={(e) => {
// Toggle with a button instead of the whole area
// We toggle using a button instead of this whole area,
// with an escape case for the header anchor link
if (!(e.target instanceof SVGElement)) {
e.preventDefault();
}

View File

@@ -1,126 +0,0 @@
'use client';
import React, {Children} from 'react';
const IllustrationContext = React.createContext<{
isInBlock?: boolean;
}>({
isInBlock: false,
});
function AuthorCredit({
author = 'Rachel Lee Nabors',
authorLink = 'https://nearestnabors.com/',
}: {
author: string;
authorLink: string;
}) {
return (
<div className="sr-only group-hover:not-sr-only group-focus-within:not-sr-only hover:sr-only">
<p className="bg-card dark:bg-card-dark text-center text-sm text-secondary dark:text-secondary-dark leading-tight p-2 rounded-lg absolute start-1/2 -top-4 -translate-x-1/2 -translate-y-full group-hover:flex group-hover:opacity-100 after:content-[''] after:absolute after:start-1/2 after:top-[95%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-card after:dark:border-t-card-dark opacity-0 transition-opacity duration-300">
<cite>
Illustrated by{' '}
{authorLink ? (
<a
target="_blank"
rel="noreferrer"
className="text-link dark:text-link-dark"
href={authorLink}>
{author}
</a>
) : (
author
)}
</cite>
</p>
</div>
);
}
export function Illustration({
caption,
src,
alt,
author,
authorLink,
}: {
caption: string;
src: string;
alt: string;
author: string;
authorLink: string;
}) {
const {isInBlock} = React.useContext(IllustrationContext);
return (
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
<figure className="my-8 flex justify-center">
<img
src={src}
alt={alt}
style={{maxHeight: 300}}
className="rounded-lg"
/>
{caption ? (
<figcaption className="text-center leading-tight mt-4">
{caption}
</figcaption>
) : null}
</figure>
{!isInBlock && <AuthorCredit author={author} authorLink={authorLink} />}
</div>
);
}
const isInBlockTrue = {isInBlock: true};
export function IllustrationBlock({
sequential,
author,
authorLink,
children,
}: {
author: string;
authorLink: string;
sequential: boolean;
children: any;
}) {
const imageInfos = Children.toArray(children).map(
(child: any) => child.props
);
const images = imageInfos.map((info, index) => (
<figure key={index}>
<div className="bg-white rounded-lg p-4 flex-1 flex xl:p-6 justify-center items-center my-4">
<img
className="text-primary"
src={info.src}
alt={info.alt}
height={info.height}
/>
</div>
{info.caption ? (
<figcaption className="text-secondary dark:text-secondary-dark text-center leading-tight mt-4">
{info.caption}
</figcaption>
) : null}
</figure>
));
return (
<IllustrationContext.Provider value={isInBlockTrue}>
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
{sequential ? (
<ol className="mdx-illustration-block flex">
{images.map((x: any, i: number) => (
<li className="flex-1" key={i}>
{x}
</li>
))}
</ol>
) : (
<div className="mdx-illustration-block">{images}</div>
)}
<AuthorCredit author={author} authorLink={authorLink} />
</div>
</IllustrationContext.Provider>
);
}

View File

@@ -1,18 +1,18 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import cn from 'classnames';
import {useContext, type HTMLAttributes} from 'react';
import {LinkContext} from './Link';
import type {HTMLAttributes} from 'react';
interface InlineCodeProps {
isLink?: boolean;
meta?: string;
}
function InlineCode({...props}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
const isLink = useContext(LinkContext);
function InlineCode({
isLink,
...props
}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
return (
<code
dir="ltr" // This is needed to prevent the code from inheriting the RTL direction of <html> in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`

View File

@@ -1,60 +0,0 @@
'use client';
// import Link from 'next/link';
import Link from './Link';
import {useContext, useMemo} from 'react';
import {Toc, TocContext, TocItem} from './TocContext';
import {UL, LI} from './Primitives';
type NestedTocRoot = {
item: null;
children: Array<NestedTocNode>;
};
type NestedTocNode = {
item: TocItem;
children: Array<NestedTocNode>;
};
function calculateNestedToc(toc: Toc): NestedTocRoot {
const currentAncestors = new Map<number, NestedTocNode | NestedTocRoot>();
const root: NestedTocRoot = {
item: null,
children: [],
};
const startIndex = 1; // Skip "Overview"
for (let i = startIndex; i < toc.length; i++) {
const item = toc[i];
const currentParent: NestedTocNode | NestedTocRoot =
currentAncestors.get(item.depth - 1) || root;
const node: NestedTocNode = {
item,
children: [],
};
currentParent.children.push(node);
currentAncestors.set(item.depth, node);
}
return root;
}
export function InlineToc() {
const toc = useContext(TocContext);
const root = useMemo(() => calculateNestedToc(toc), [toc]);
if (root.children.length < 2) {
return null;
}
return <InlineTocItem items={root.children} />;
}
function InlineTocItem({items}: {items: Array<NestedTocNode>}) {
return (
<UL>
{items.map((node) => (
<LI key={node.item.url}>
<Link href={node.item.url}>{node.item.node}</Link>
{node.children.length > 0 && <InlineTocItem items={node.children} />}
</LI>
))}
</UL>
);
}

View File

@@ -1,39 +0,0 @@
'use client';
import Link from './Link';
import React from 'react';
import {finishedTranslations} from 'utils/finishedTranslations';
import {LanguagesContext} from './LanguagesContext';
import {UL, LI} from './Primitives';
type TranslationProgress = 'complete' | 'in-progress';
export function LanguageList({progress}: {progress: TranslationProgress}) {
const allLanguages = React.useContext(LanguagesContext) ?? [];
const languages = allLanguages
.filter(
({code}) =>
code !== 'en' &&
(progress === 'complete'
? finishedTranslations.includes(code)
: !finishedTranslations.includes(code))
)
.sort((a, b) => a.enName.localeCompare(b.enName));
return (
<UL>
{languages.map(({code, name, enName}) => {
return (
<LI key={code}>
<Link href={`https://${code}.react.dev/`}>
{enName} ({name})
</Link>{' '}
&mdash;{' '}
<Link href={`https://github.com/reactjs/${code}.react.dev`}>
Contribute
</Link>
</LI>
);
})}
</UL>
);
}

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -1,17 +1,13 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {createContext} from 'react';
import {Children, cloneElement} from 'react';
import NextLink from 'next/link';
import cn from 'classnames';
import {ExternalLink} from 'components/ExternalLink';
export const LinkContext = createContext(false);
function Link({
href,
className,
@@ -20,29 +16,36 @@ function Link({
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const classes =
'inline text-link dark:text-link-dark border-b border-link border-opacity-0 hover:border-opacity-100 duration-100 ease-in transition leading-normal';
const modifiedChildren = Children.toArray(children).map((child: any) => {
if (child.type?.mdxName && child.type?.mdxName === 'inlineCode') {
return cloneElement(child, {
isLink: true,
});
}
return child;
});
if (!href) {
// eslint-disable-next-line jsx-a11y/anchor-has-content
return <a href={href} className={className} {...props} />;
}
return (
<LinkContext.Provider value={true}>
<>
{href.startsWith('https://') ? (
<ExternalLink href={href} className={cn(classes, className)} {...props}>
{children}
{modifiedChildren}
</ExternalLink>
) : href.startsWith('#') ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a className={cn(classes, className)} href={href} {...props}>
{children}
{modifiedChildren}
</a>
) : (
<NextLink href={href} className={cn(classes, className)} {...props}>
{children}
{modifiedChildren}
</NextLink>
)}
</LinkContext.Provider>
</>
);
}

View File

@@ -1,10 +1,8 @@
// 'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
// import {Children, useContext, useMemo} from 'react';
import {Children, useContext, useMemo} from 'react';
import * as React from 'react';
import cn from 'classnames';
import type {HTMLAttributes} from 'react';
@@ -31,13 +29,14 @@ import YouWillLearnCard from './YouWillLearnCard';
import {Challenges, Hint, Solution} from './Challenges';
import {IconNavArrow} from '../Icon/IconNavArrow';
import ButtonLink from 'components/ButtonLink';
import {TocContext} from './TocContext';
import type {Toc, TocItem} from './TocContext';
import {TeamMember} from './TeamMember';
import {LanguagesContext} from './LanguagesContext';
import {finishedTranslations} from 'utils/finishedTranslations';
import ErrorDecoder from './ErrorDecoder';
import {IconCanary} from '../Icon/IconCanary';
import {InlineToc} from './InlineToc';
import {Illustration, IllustrationBlock} from './Illustration';
import {LanguageList} from './LanguageList';
import {Divider, LI, OL, P, Strong, UL} from './Primitives';
function CodeStep({children, step}: {children: any; step: number}) {
return (
@@ -61,6 +60,27 @@ function CodeStep({children, step}: {children: any; step: number}) {
);
}
const P = (p: HTMLAttributes<HTMLParagraphElement>) => (
<p className="whitespace-pre-wrap my-4" {...p} />
);
const Strong = (strong: HTMLAttributes<HTMLElement>) => (
<strong className="font-bold" {...strong} />
);
const OL = (p: HTMLAttributes<HTMLOListElement>) => (
<ol className="ms-6 my-3 list-decimal" {...p} />
);
const LI = (p: HTMLAttributes<HTMLLIElement>) => (
<li className="leading-relaxed mb-1" {...p} />
);
const UL = (p: HTMLAttributes<HTMLUListElement>) => (
<ul className="ms-6 my-3 list-disc" {...p} />
);
const Divider = () => (
<hr className="my-6 block border-b border-t-0 border-border dark:border-border-dark" />
);
const Wip = ({children}: {children: React.ReactNode}) => (
<ExpandableCallout type="wip">{children}</ExpandableCallout>
);
@@ -212,6 +232,214 @@ function Recipes(props: any) {
return <Challenges {...props} isRecipes={true} />;
}
function AuthorCredit({
author = 'Rachel Lee Nabors',
authorLink = 'https://nearestnabors.com/',
}: {
author: string;
authorLink: string;
}) {
return (
<div className="sr-only group-hover:not-sr-only group-focus-within:not-sr-only hover:sr-only">
<p className="bg-card dark:bg-card-dark text-center text-sm text-secondary dark:text-secondary-dark leading-tight p-2 rounded-lg absolute start-1/2 -top-4 -translate-x-1/2 -translate-y-full group-hover:flex group-hover:opacity-100 after:content-[''] after:absolute after:start-1/2 after:top-[95%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-card after:dark:border-t-card-dark opacity-0 transition-opacity duration-300">
<cite>
Illustrated by{' '}
{authorLink ? (
<a
target="_blank"
rel="noreferrer"
className="text-link dark:text-link-dark"
href={authorLink}>
{author}
</a>
) : (
author
)}
</cite>
</p>
</div>
);
}
const IllustrationContext = React.createContext<{
isInBlock?: boolean;
}>({
isInBlock: false,
});
function Illustration({
caption,
src,
alt,
author,
authorLink,
}: {
caption: string;
src: string;
alt: string;
author: string;
authorLink: string;
}) {
const {isInBlock} = React.useContext(IllustrationContext);
return (
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
<figure className="my-8 flex justify-center">
<img
src={src}
alt={alt}
style={{maxHeight: 300}}
className="rounded-lg"
/>
{caption ? (
<figcaption className="text-center leading-tight mt-4">
{caption}
</figcaption>
) : null}
</figure>
{!isInBlock && <AuthorCredit author={author} authorLink={authorLink} />}
</div>
);
}
const isInBlockTrue = {isInBlock: true};
function IllustrationBlock({
sequential,
author,
authorLink,
children,
}: {
author: string;
authorLink: string;
sequential: boolean;
children: any;
}) {
const imageInfos = Children.toArray(children).map(
(child: any) => child.props
);
const images = imageInfos.map((info, index) => (
<figure key={index}>
<div className="bg-white rounded-lg p-4 flex-1 flex xl:p-6 justify-center items-center my-4">
<img
className="text-primary"
src={info.src}
alt={info.alt}
height={info.height}
/>
</div>
{info.caption ? (
<figcaption className="text-secondary dark:text-secondary-dark text-center leading-tight mt-4">
{info.caption}
</figcaption>
) : null}
</figure>
));
return (
<IllustrationContext.Provider value={isInBlockTrue}>
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
{sequential ? (
<ol className="mdx-illustration-block flex">
{images.map((x: any, i: number) => (
<li className="flex-1" key={i}>
{x}
</li>
))}
</ol>
) : (
<div className="mdx-illustration-block">{images}</div>
)}
<AuthorCredit author={author} authorLink={authorLink} />
</div>
</IllustrationContext.Provider>
);
}
type NestedTocRoot = {
item: null;
children: Array<NestedTocNode>;
};
type NestedTocNode = {
item: TocItem;
children: Array<NestedTocNode>;
};
function calculateNestedToc(toc: Toc): NestedTocRoot {
const currentAncestors = new Map<number, NestedTocNode | NestedTocRoot>();
const root: NestedTocRoot = {
item: null,
children: [],
};
const startIndex = 1; // Skip "Overview"
for (let i = startIndex; i < toc.length; i++) {
const item = toc[i];
const currentParent: NestedTocNode | NestedTocRoot =
currentAncestors.get(item.depth - 1) || root;
const node: NestedTocNode = {
item,
children: [],
};
currentParent.children.push(node);
currentAncestors.set(item.depth, node);
}
return root;
}
function InlineToc() {
const toc = useContext(TocContext);
const root = useMemo(() => calculateNestedToc(toc), [toc]);
if (root.children.length < 2) {
return null;
}
return <InlineTocItem items={root.children} />;
}
function InlineTocItem({items}: {items: Array<NestedTocNode>}) {
return (
<UL>
{items.map((node) => (
<LI key={node.item.url}>
<Link href={node.item.url}>{node.item.text}</Link>
{node.children.length > 0 && <InlineTocItem items={node.children} />}
</LI>
))}
</UL>
);
}
type TranslationProgress = 'complete' | 'in-progress';
function LanguageList({progress}: {progress: TranslationProgress}) {
const allLanguages = React.useContext(LanguagesContext) ?? [];
const languages = allLanguages
.filter(
({code}) =>
code !== 'en' &&
(progress === 'complete'
? finishedTranslations.includes(code)
: !finishedTranslations.includes(code))
)
.sort((a, b) => a.enName.localeCompare(b.enName));
return (
<UL>
{languages.map(({code, name, enName}) => {
return (
<LI key={code}>
<Link href={`https://${code}.react.dev/`}>
{enName} ({name})
</Link>{' '}
&mdash;{' '}
<Link href={`https://github.com/reactjs/${code}.react.dev`}>
Contribute
</Link>
</LI>
);
})}
</UL>
);
}
function YouTubeIframe(props: any) {
return (
<div className="relative h-0 overflow-hidden pt-[56.25%]">
@@ -232,22 +460,7 @@ function Image(props: any) {
return <img alt={alt} className="max-w-[calc(min(700px,100%))]" {...rest} />;
}
function annotateMDXComponents(
components: Record<string, React.ElementType>
): Record<string, React.ElementType> {
return Object.entries(components).reduce((acc, [key, Component]) => {
acc[key] = (props) => <Component {...props} data-mdx-name={key} />;
acc[key].displayName = `Annotated(${key})`; // Optional, for debugging
return acc;
}, {} as Record<string, React.ElementType>);
}
export const MDXComponentsToc = annotateMDXComponents({
a: Link,
code: InlineCode,
});
export const MDXComponents = annotateMDXComponents({
export const MDXComponents = {
p: P,
strong: Strong,
blockquote: Blockquote,
@@ -316,4 +529,11 @@ export const MDXComponents = annotateMDXComponents({
CodeStep,
YouTubeIframe,
ErrorDecoder,
});
};
for (let key in MDXComponents) {
if (MDXComponents.hasOwnProperty(key)) {
const MDXComponent: any = (MDXComponents as any)[key];
MDXComponent.mdxName = key;
}
}

View File

@@ -12,10 +12,10 @@ interface PackageImportProps {
export function PackageImport({children}: PackageImportProps) {
const terminal = Children.toArray(children).filter((child: any) => {
return child.props?.['data-mdx-name'] !== 'pre';
return child.type?.mdxName !== 'pre';
});
const code = Children.toArray(children).map((child: any, i: number) => {
if (child.props?.['data-mdx-name'] === 'pre') {
if (child.type?.mdxName === 'pre') {
return (
<CodeBlock
{...child.props}

View File

@@ -1,23 +0,0 @@
import {HTMLAttributes} from 'react';
export const P = (p: HTMLAttributes<HTMLParagraphElement>) => (
<p className="whitespace-pre-wrap my-4" {...p} />
);
export const Strong = (strong: HTMLAttributes<HTMLElement>) => (
<strong className="font-bold" {...strong} />
);
export const OL = (p: HTMLAttributes<HTMLOListElement>) => (
<ol className="ms-6 my-3 list-decimal" {...p} />
);
export const LI = (p: HTMLAttributes<HTMLLIElement>) => (
<li className="leading-relaxed mb-1" {...p} />
);
export const UL = (p: HTMLAttributes<HTMLUListElement>) => (
<ul className="ms-6 my-3 list-disc" {...p} />
);
export const Divider = () => (
<hr className="my-6 block border-b border-t-0 border-border dark:border-border-dark" />
);

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -12,22 +12,19 @@ export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];
export const createFileMap = (codeSnippets: any) => {
return codeSnippets.reduce(
(result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {
// TODO: actually fix this
if (
(codeSnippet.type as any).mdxName !== 'pre' &&
codeSnippet.type !== 'pre'
) {
return result;
}
const {props} = (
codeSnippet.props as PropsWithChildren<{
children: ReactElement<
HTMLAttributes<HTMLDivElement> & {
meta?: string;
'data-mdx-name'?: string;
}
HTMLAttributes<HTMLDivElement> & {meta?: string}
>;
}>
).children;
if (props?.['data-mdx-name'] !== 'code') {
return result;
}
let filePath; // path in the folder structure
let fileHidden = false; // if the file is available as a tab
let fileActive = false; // if the file tab is shown by default

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -1,5 +1,3 @@
'use client';
import {Children, memo} from 'react';
import InlineCode from './InlineCode';
import Sandpack from './Sandpack';

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

View File

@@ -7,7 +7,7 @@ import type {ReactNode} from 'react';
export type TocItem = {
url: string;
node: ReactNode;
text: ReactNode;
depth: number;
};
export type Toc = Array<TocItem>;

View File

@@ -1,22 +0,0 @@
'use client';
import {useEffect} from 'react';
export function ScrollHandler() {
useEffect(() => {
// Taken from StackOverflow. Trying to detect both Safari desktop and mobile.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// This is kind of a lie.
// We still rely on the manual Next.js scrollRestoration logic.
// However, we *also* don't want Safari grey screen during the back swipe gesture.
// Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.
history.scrollRestoration = 'auto';
} else {
// For other browsers, let Next.js set scrollRestoration to 'manual'.
// It seems to work better for Chrome and Firefox which don't animate the back swipe.
}
}, []);
return null;
}

View File

@@ -4,7 +4,7 @@
import Head from 'next/head';
import Link from 'next/link';
import {useRouter} from 'next/navigation';
import Router from 'next/router';
import {lazy, useEffect} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';
@@ -111,7 +111,6 @@ export function Search({
},
}: SearchProps) {
useDocSearchKeyboardEvents({isOpen, onOpen, onClose});
const router = useRouter();
return (
<>
<Head>
@@ -128,7 +127,7 @@ export function Search({
onClose={onClose}
navigator={{
navigate({itemUrl}: any) {
router.push(itemUrl);
Router.push(itemUrl);
},
}}
transformItems={(items: any[]) => {

185
src/components/Seo.tsx Normal file
View File

@@ -0,0 +1,185 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import * as React from 'react';
import Head from 'next/head';
import {withRouter, Router} from 'next/router';
import {siteConfig} from '../siteConfig';
import {finishedTranslations} from 'utils/finishedTranslations';
export interface SeoProps {
title: string;
titleForTitleTag: undefined | string;
description?: string;
image?: string;
// jsonld?: JsonLDType | Array<JsonLDType>;
children?: React.ReactNode;
isHomePage: boolean;
searchOrder?: number;
}
// If you are a maintainer of a language fork,
// deployedTranslations has been moved to src/utils/finishedTranslations.ts.
function getDomain(languageCode: string): string {
const subdomain = languageCode === 'en' ? '' : languageCode + '.';
return subdomain + 'react.dev';
}
export const Seo = withRouter(
({
title,
titleForTitleTag,
image = '/images/og-default.png',
router,
children,
isHomePage,
searchOrder,
}: SeoProps & {router: Router}) => {
const siteDomain = getDomain(siteConfig.languageCode);
const canonicalUrl = `https://${siteDomain}${
router.asPath.split(/[\?\#]/)[0]
}`;
// Allow setting a different title for Google results
const pageTitle =
(titleForTitleTag ?? title) + (isHomePage ? '' : ' React');
// Twitter's meta parser is not very good.
const twitterTitle = pageTitle.replace(/[<>]/g, '');
let description = isHomePage
? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.'
: 'The library for web and native user interfaces';
return (
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{title != null && <title key="title">{pageTitle}</title>}
{isHomePage && (
// Let Google figure out a good description for each page.
<meta name="description" key="description" content={description} />
)}
<link rel="canonical" href={canonicalUrl} />
<link
rel="alternate"
href={canonicalUrl.replace(siteDomain, getDomain('en'))}
hrefLang="x-default"
/>
{finishedTranslations.map((languageCode) => (
<link
key={'alt-' + languageCode}
rel="alternate"
hrefLang={languageCode}
href={canonicalUrl.replace(siteDomain, getDomain(languageCode))}
/>
))}
<meta property="fb:app_id" content="623268441017527" />
<meta property="og:type" key="og:type" content="website" />
<meta property="og:url" key="og:url" content={canonicalUrl} />
{title != null && (
<meta property="og:title" content={pageTitle} key="og:title" />
)}
{description != null && (
<meta
property="og:description"
key="og:description"
content={description}
/>
)}
<meta
property="og:image"
key="og:image"
content={`https://${siteDomain}${image}`}
/>
<meta
name="twitter:card"
key="twitter:card"
content="summary_large_image"
/>
<meta name="twitter:site" key="twitter:site" content="@reactjs" />
<meta name="twitter:creator" key="twitter:creator" content="@reactjs" />
{title != null && (
<meta
name="twitter:title"
key="twitter:title"
content={twitterTitle}
/>
)}
{description != null && (
<meta
name="twitter:description"
key="twitter:description"
content={description}
/>
)}
<meta
name="twitter:image"
key="twitter:image"
content={`https://${siteDomain}${image}`}
/>
<meta
name="google-site-verification"
content="sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0"
/>
{searchOrder != null && (
<meta name="algolia-search-order" content={'' + searchOrder} />
)}
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Display_W_Md.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Display_W_SBd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Display_W_Bd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Text_W_Md.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Text_W_Bd.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Text_W_Rg.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="https://react.dev/fonts/Optimistic_Text_W_It.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
{children}
</Head>
);
}
);

View File

@@ -1,52 +0,0 @@
function ThemeInlineScript() {
function setTheme(newTheme) {
window.__theme = newTheme;
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (newTheme === 'light') {
document.documentElement.classList.remove('dark');
}
}
var preferredTheme;
try {
preferredTheme = localStorage.getItem('theme');
} catch (err) {}
window.__setPreferredTheme = function (newTheme) {
preferredTheme = newTheme;
setTheme(newTheme);
try {
localStorage.setItem('theme', newTheme);
} catch (err) {}
};
var initialTheme = preferredTheme;
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (!initialTheme) {
initialTheme = darkQuery.matches ? 'dark' : 'light';
}
setTheme(initialTheme);
darkQuery.addEventListener('change', function (e) {
if (!preferredTheme) {
setTheme(e.matches ? 'dark' : 'light');
}
});
document.documentElement.classList.add(
window.navigator.platform.includes('Mac') ? 'platform-mac' : 'platform-win'
);
}
export function ThemeScript() {
return (
<script
id="theme-script"
dangerouslySetInnerHTML={{
__html: `(${ThemeInlineScript.toString()})()`,
}}
/>
);
}

View File

@@ -1,64 +0,0 @@
function UwuInlineScript() {
try {
let logShown = false;
function setUwu(isUwu) {
try {
if (isUwu) {
localStorage.setItem('uwu', true);
document.documentElement.classList.add('uwu');
if (!logShown) {
console.log('uwu mode! turn off with ?uwu=0');
console.log(
'logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/ServiceLogos'
);
logShown = true;
}
} else {
localStorage.removeItem('uwu');
document.documentElement.classList.remove('uwu');
console.log('uwu mode off. turn on with ?uwu');
}
} catch (err) {}
}
window.__setUwu = setUwu;
function checkQueryParam() {
const params = new URLSearchParams(window.location.search);
const value = params.get('uwu');
switch (value) {
case '':
case 'true':
case '1':
return true;
case 'false':
case '0':
return false;
default:
return null;
}
}
function checkLocalStorage() {
try {
return localStorage.getItem('uwu') === 'true';
} catch (err) {
return false;
}
}
const uwuQueryParam = checkQueryParam();
if (uwuQueryParam != null) {
setUwu(uwuQueryParam);
} else if (checkLocalStorage()) {
document.documentElement.classList.add('uwu');
}
} catch (err) {}
}
export function UwuScript() {
return (
<script
id="uwu-script"
dangerouslySetInnerHTML={{
__html: `(${UwuInlineScript.toString()})()`,
}}
/>
);
}

View File

@@ -38,6 +38,5 @@ React documentation is written and maintained by the [React team](/community/tea
* [Rick Hanlon](https://twitter.com/rickhanlonii): site development
* [Harish Kumar](https://www.strek.in/): development and maintenance
* [Luna Ruan](https://twitter.com/lunaruan): sandbox improvements
* [Jimmy Lai](https://twitter.com/feedthejim): site development
We'd also like to thank countless alpha testers and community members who gave us feedback along the way.

View File

@@ -298,4 +298,4 @@ See the first blog post: [Why did we build React?](https://legacy.reactjs.org/bl
React was open sourced at Facebook Seattle in 2013:
<iframe width="560" height="315" src="https://www.youtube.com/embed/XxVg_s8xAms?si=466vSJrnXTn05j9A" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe>
<iframe width="560" height="315" src="https://www.youtube.com/embed/XxVg_s8xAms?si=466vSJrnXTn05j9A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -2,9 +2,40 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {useRouter} from 'next/router';
import {useState, useRef, useEffect} from 'react';
const usePendingRoute = () => {
// TODO: @feedthejim - Implement usePendingRoute when App Router supports tapping into the transition state
return null;
const {events} = useRouter();
const [pendingRoute, setPendingRoute] = useState<string | null>(null);
const currentRoute = useRef<string | null>(null);
useEffect(() => {
let routeTransitionTimer: any = null;
const handleRouteChangeStart = (url: string) => {
clearTimeout(routeTransitionTimer);
routeTransitionTimer = setTimeout(() => {
if (currentRoute.current !== url) {
currentRoute.current = url;
setPendingRoute(url);
}
}, 100);
};
const handleRouteChangeComplete = () => {
setPendingRoute(null);
clearTimeout(routeTransitionTimer);
};
events.on('routeChangeStart', handleRouteChangeStart);
events.on('routeChangeComplete', handleRouteChangeComplete);
return () => {
events.off('routeChangeStart', handleRouteChangeStart);
events.off('routeChangeComplete', handleRouteChangeComplete);
clearTimeout(routeTransitionTimer);
};
}, [events]);
return pendingRoute;
};
export default usePendingRoute;

View File

@@ -1,34 +0,0 @@
export function register() {
if (
process.env.NODE_ENV === 'development' &&
process.env.NEXT_RUNTIME === 'nodejs'
) {
// watch for changes in the ./src/content directory
// and trigger an HMR update when a change is detected via a custom WebSocket setup
const chokidar = require('chokidar');
const path = require('path');
const ws = require('ws');
const wsServer = new ws.Server({
port: 3001,
});
function triggerRefresh() {
wsServer.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({event: 'refresh'}));
}
});
}
// the process is in .next so we need to go up two level
const contentDir = path.resolve(__dirname, '../../src/content');
const watcher = chokidar.watch(contentDir, {
ignoreInitial: true,
});
watcher.on('all', () => {
triggerRefresh();
});
}
}

View File

@@ -5,19 +5,12 @@
import {Page} from 'components/Layout/Page';
import {MDXComponents} from 'components/MDX/MDXComponents';
import sidebarLearn from '../sidebarLearn.json';
import {RouteItem} from 'components/Layout/getRouteMeta';
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
export default function NotFound() {
return (
<Page
toc={[]}
pathname="/404"
section="unknown"
meta={{title: 'Not Found'}}
routeTree={sidebarLearn as RouteItem}>
<Page toc={[]} meta={{title: 'Not Found'}} routeTree={sidebarLearn}>
<MaxWidth>
<Intro>
<P>This page doesnt exist.</P>
@@ -34,11 +27,3 @@ export default function NotFound() {
</Page>
);
}
export async function generateMetadata({}: {}) {
return generateSeoMetadata({
title: 'Not Found',
isHomePage: false,
path: '/404',
});
}

View File

@@ -1,5 +1,3 @@
'use client';
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
@@ -7,18 +5,14 @@
import {Page} from 'components/Layout/Page';
import {MDXComponents} from 'components/MDX/MDXComponents';
import sidebarLearn from '../sidebarLearn.json';
import {RouteItem} from 'components/Layout/getRouteMeta';
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
export default function Error() {
export default function NotFound() {
return (
<Page
section="unknown"
toc={[]}
pathname="/500"
routeTree={sidebarLearn as RouteItem}
routeTree={sidebarLearn}
meta={{title: 'Something Went Wrong'}}>
<MaxWidth>
<Intro>
@@ -35,11 +29,3 @@ export default function Error() {
</Page>
);
}
export async function generateMetadata({}: {}) {
return generateSeoMetadata({
title: 'Something Went Wrong',
isHomePage: false,
path: '/500',
});
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {Fragment, useMemo} from 'react';
import {useRouter} from 'next/router';
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';
import {MDXComponents} from 'components/MDX/MDXComponents';
import compileMDX from 'utils/compileMDX';
import {generateRssFeed} from '../utils/rss';
export default function Layout({content, toc, meta, languages}) {
const parsedContent = useMemo(
() => JSON.parse(content, reviveNodeOnClient),
[content]
);
const parsedToc = useMemo(() => JSON.parse(toc, reviveNodeOnClient), [toc]);
const section = useActiveSection();
let routeTree;
switch (section) {
case 'home':
case 'unknown':
routeTree = sidebarHome;
break;
case 'learn':
routeTree = sidebarLearn;
break;
case 'reference':
routeTree = sidebarReference;
break;
case 'community':
routeTree = sidebarCommunity;
break;
case 'blog':
routeTree = sidebarBlog;
break;
}
return (
<Page
toc={parsedToc}
routeTree={routeTree}
meta={meta}
section={section}
languages={languages}>
{parsedContent}
</Page>
);
}
function useActiveSection() {
const {asPath} = useRouter();
const cleanedPath = asPath.split(/[\?\#]/)[0];
if (cleanedPath === '/') {
return 'home';
} else if (cleanedPath.startsWith('/reference')) {
return 'reference';
} else if (asPath.startsWith('/learn')) {
return 'learn';
} else if (asPath.startsWith('/community')) {
return 'community';
} else if (asPath.startsWith('/blog')) {
return 'blog';
} else {
return 'unknown';
}
}
// Deserialize a client React tree from JSON.
function reviveNodeOnClient(parentPropertyName, val) {
if (Array.isArray(val) && val[0] == '$r') {
// Assume it's a React element.
let Type = val[1];
let key = val[2];
if (key == null) {
key = parentPropertyName; // Index within a parent.
}
let props = val[3];
if (Type === 'wrapper') {
Type = Fragment;
props = {children: props.children};
}
if (Type in MDXComponents) {
Type = MDXComponents[Type];
}
if (!Type) {
console.error('Unknown type: ' + Type);
Type = Fragment;
}
return <Type key={key} {...props} />;
} else {
return val;
}
}
// Put MDX output into JSON for client.
export async function getStaticProps(context) {
generateRssFeed();
const fs = require('fs');
const rootDir = process.cwd() + '/src/content/';
// Read MDX from the file.
let path = (context.params.markdownPath || []).join('/') || 'index';
let mdx;
try {
mdx = fs.readFileSync(rootDir + path + '.md', 'utf8');
} catch {
mdx = fs.readFileSync(rootDir + path + '/index.md', 'utf8');
}
const {toc, content, meta, languages} = await compileMDX(mdx, path, {});
return {
props: {
toc,
content,
meta,
languages,
},
};
}
// Collect all MDX files for static generation.
export async function getStaticPaths() {
const {promisify} = require('util');
const {resolve} = require('path');
const fs = require('fs');
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const rootDir = process.cwd() + '/src/content';
// Find all MD files recursively.
async function getFiles(dir) {
const subdirs = await readdir(dir);
const files = await Promise.all(
subdirs.map(async (subdir) => {
const res = resolve(dir, subdir);
return (await stat(res)).isDirectory()
? getFiles(res)
: res.slice(rootDir.length + 1);
})
);
return (
files
.flat()
// ignores `errors/*.md`, they will be handled by `pages/errors/[errorCode].tsx`
.filter((file) => file.endsWith('.md') && !file.startsWith('errors/'))
);
}
// 'foo/bar/baz.md' -> ['foo', 'bar', 'baz']
// 'foo/bar/qux/index.md' -> ['foo', 'bar', 'qux']
function getSegments(file) {
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
if (segments[segments.length - 1] === 'index') {
segments.pop();
}
return segments;
}
const files = await getFiles(rootDir);
const paths = files.map((file) => ({
params: {
markdownPath: getSegments(file),
// ^^^ CAREFUL HERE.
// If you rename markdownPath, update patches/next-remote-watch.patch too.
// Otherwise you'll break Fast Refresh for all MD files.
},
}));
return {
paths: paths,
fallback: false,
};
}

58
src/pages/_app.tsx Normal file
View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {useEffect} from 'react';
import {AppProps} from 'next/app';
import {useRouter} from 'next/router';
import '@docsearch/css';
import '../styles/algolia.css';
import '../styles/index.css';
import '../styles/sandpack.css';
if (typeof window !== 'undefined') {
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
window.addEventListener(terminationEvent, function () {
// @ts-ignore
gtag('event', 'timing', {
event_label: 'JS Dependencies',
event: 'unload',
});
});
}
export default function MyApp({Component, pageProps}: AppProps) {
const router = useRouter();
useEffect(() => {
// Taken from StackOverflow. Trying to detect both Safari desktop and mobile.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// This is kind of a lie.
// We still rely on the manual Next.js scrollRestoration logic.
// However, we *also* don't want Safari grey screen during the back swipe gesture.
// Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.
history.scrollRestoration = 'auto';
} else {
// For other browsers, let Next.js set scrollRestoration to 'manual'.
// It seems to work better for Chrome and Firefox which don't animate the back swipe.
}
}, []);
useEffect(() => {
const handleRouteChange = (url: string) => {
const cleanedUrl = url.split(/[\?\#]/)[0];
// @ts-ignore
gtag('event', 'pageview', {
event_label: cleanedUrl,
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}

158
src/pages/_document.tsx Normal file
View File

@@ -0,0 +1,158 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {Html, Head, Main, NextScript} from 'next/document';
import {siteConfig} from '../siteConfig';
const MyDocument = () => {
return (
<Html lang={siteConfig.languageCode} dir={siteConfig.isRTL ? 'rtl' : 'ltr'}>
<Head />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#404756" />
<meta name="msapplication-TileColor" content="#2b5797" />
<meta name="theme-color" content="#23272f" />
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${process.env.NEXT_PUBLIC_GA_TRACKING_ID}');`,
}}
/>
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
<script
dangerouslySetInnerHTML={{
__html: `
(function () {
try {
let logShown = false;
function setUwu(isUwu) {
try {
if (isUwu) {
localStorage.setItem('uwu', true);
document.documentElement.classList.add('uwu');
if (!logShown) {
console.log('uwu mode! turn off with ?uwu=0');
console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/ServiceLogos');
logShown = true;
}
} else {
localStorage.removeItem('uwu');
document.documentElement.classList.remove('uwu');
console.log('uwu mode off. turn on with ?uwu');
}
} catch (err) { }
}
window.__setUwu = setUwu;
function checkQueryParam() {
const params = new URLSearchParams(window.location.search);
const value = params.get('uwu');
switch(value) {
case '':
case 'true':
case '1':
return true;
case 'false':
case '0':
return false;
default:
return null;
}
}
function checkLocalStorage() {
try {
return localStorage.getItem('uwu') === 'true';
} catch (err) {
return false;
}
}
const uwuQueryParam = checkQueryParam();
if (uwuQueryParam != null) {
setUwu(uwuQueryParam);
} else if (checkLocalStorage()) {
document.documentElement.classList.add('uwu');
}
} catch (err) { }
})();
`,
}}
/>
<script
dangerouslySetInnerHTML={{
__html: `
(function () {
function setTheme(newTheme) {
window.__theme = newTheme;
if (newTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (newTheme === 'light') {
document.documentElement.classList.remove('dark');
}
}
var preferredTheme;
try {
preferredTheme = localStorage.getItem('theme');
} catch (err) { }
window.__setPreferredTheme = function(newTheme) {
preferredTheme = newTheme;
setTheme(newTheme);
try {
localStorage.setItem('theme', newTheme);
} catch (err) { }
};
var initialTheme = preferredTheme;
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (!initialTheme) {
initialTheme = darkQuery.matches ? 'dark' : 'light';
}
setTheme(initialTheme);
darkQuery.addEventListener('change', function (e) {
if (!preferredTheme) {
setTheme(e.matches ? 'dark' : 'light');
}
});
// Detect whether the browser is Mac to display platform specific content
// An example of such content can be the keyboard shortcut displayed in the search bar
document.documentElement.classList.add(
window.navigator.platform.includes('Mac')
? "platform-mac"
: "platform-win"
);
})();
`,
}}
/>
<Main />
<NextScript />
</body>
</Html>
);
};
export default MyDocument;

View File

@@ -0,0 +1,153 @@
import {Fragment, useMemo} from 'react';
import {Page} from 'components/Layout/Page';
import {MDXComponents} from 'components/MDX/MDXComponents';
import sidebarLearn from 'sidebarLearn.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next';
import {ErrorDecoderContext} from 'components/ErrorDecoderContext';
import compileMDX from 'utils/compileMDX';
interface ErrorDecoderProps {
errorCode: string | null;
errorMessage: string | null;
content: string;
toc: string;
meta: any;
}
export default function ErrorDecoderPage({
errorMessage,
errorCode,
content,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const parsedContent = useMemo<React.ReactNode>(
() => JSON.parse(content, reviveNodeOnClient),
[content]
);
return (
<ErrorDecoderContext.Provider value={{errorMessage, errorCode}}>
<Page
toc={[]}
meta={{
title: errorCode
? `Minified React error #${errorCode}`
: 'Minified Error Decoder',
}}
routeTree={sidebarLearn as RouteItem}
section="unknown">
<div className="whitespace-pre-line">{parsedContent}</div>
{/* <MaxWidth>
<P>
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.
</P>
<ErrorDecoder />
</MaxWidth> */}
</Page>
</ErrorDecoderContext.Provider>
);
}
// Deserialize a client React tree from JSON.
function reviveNodeOnClient(parentPropertyName: unknown, val: any) {
if (Array.isArray(val) && val[0] == '$r') {
// Assume it's a React element.
let Type = val[1];
let key = val[2];
if (key == null) {
key = parentPropertyName; // Index within a parent.
}
let props = val[3];
if (Type === 'wrapper') {
Type = Fragment;
props = {children: props.children};
}
if (Type in MDXComponents) {
Type = MDXComponents[Type as keyof typeof MDXComponents];
}
if (!Type) {
console.error('Unknown type: ' + Type);
Type = Fragment;
}
return <Type key={key} {...props} />;
} else {
return val;
}
}
/**
* Next.js Page Router doesn't have a way to cache specific data fetching request.
* But since Next.js uses limited number of workers, keep "cachedErrorCodes" as a
* module level memory cache can reduce the number of requests down to once per worker.
*
* TODO: use `next/unstable_cache` when migrating to Next.js App Router
*/
let cachedErrorCodes: Record<string, string> | null = null;
export const getStaticProps: GetStaticProps<ErrorDecoderProps> = async ({
params,
}) => {
const errorCodes: {[key: string]: string} = (cachedErrorCodes ||= await (
await fetch(
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
)
).json());
const code = typeof params?.errorCode === 'string' ? params?.errorCode : null;
if (code && !errorCodes[code]) {
return {
notFound: true,
};
}
const fs = require('fs');
const rootDir = process.cwd() + '/src/content/errors';
// Read MDX from the file.
let path = params?.errorCode || 'index';
let mdx;
try {
mdx = fs.readFileSync(rootDir + '/' + path + '.md', 'utf8');
} catch {
// if [errorCode].md is not found, fallback to generic.md
mdx = fs.readFileSync(rootDir + '/generic.md', 'utf8');
}
const {content, toc, meta} = await compileMDX(mdx, path, {code, errorCodes});
return {
props: {
content,
toc,
meta,
errorCode: code,
errorMessage: code ? errorCodes[code] : null,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
/**
* Fetch error codes from GitHub
*/
const errorCodes = (cachedErrorCodes ||= await (
await fetch(
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
)
).json());
const paths = Object.keys(errorCodes).map((code) => ({
params: {
errorCode: code,
},
}));
return {
paths,
fallback: 'blocking',
};
};

View File

@@ -0,0 +1,3 @@
import ErrorDecoderPage from './[errorCode]';
export default ErrorDecoderPage;
export {getStaticProps} from './[errorCode]';

View File

@@ -19,7 +19,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -27,7 +28,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_MdIt.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_MdIt.woff2')
format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap;
@@ -35,7 +37,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_SBd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_SBd.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -43,7 +46,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_SBdIt.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_SBdIt.woff2')
format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
@@ -51,7 +55,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -59,7 +64,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_W_BdIt.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_W_BdIt.woff2')
format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
@@ -67,7 +73,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_Rg.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -75,7 +82,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_It.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_It.woff2')
format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
@@ -83,7 +91,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -91,7 +100,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_MdIt.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_MdIt.woff2')
format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap;
@@ -99,7 +109,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -107,7 +118,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_W_BdIt.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_W_BdIt.woff2')
format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
@@ -117,7 +129,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Arbc_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -126,7 +139,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Arbc_W_SBd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_SBd.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -135,7 +149,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Arbc_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -144,7 +159,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Arbc_W_Rg.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -153,7 +169,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Arbc_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -162,7 +179,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Arbc_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -173,7 +191,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Cyrl_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -182,7 +201,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Cyrl_W_SBd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_SBd.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -191,7 +211,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Cyrl_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -200,7 +221,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Cyrl_W_Rg.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -209,7 +231,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Cyrl_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -218,7 +241,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Cyrl_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -229,7 +253,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Deva_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -239,7 +264,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Deva_W_SBd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_SBd.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -249,7 +275,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Deva_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -259,7 +286,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Deva_W_Rg.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -269,7 +297,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Deva_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -279,7 +308,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Deva_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -291,7 +321,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Viet_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -300,7 +331,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Viet_W_SBd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_SBd.woff2')
format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -309,7 +341,8 @@
@font-face {
font-family: 'Optimistic Display';
src: url('/fonts/Optimistic_Display_Viet_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -318,7 +351,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Viet_W_Rg.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Rg.woff2')
format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -327,7 +361,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Viet_W_Md.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Md.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -336,7 +371,8 @@
@font-face {
font-family: 'Optimistic Text';
src: url('/fonts/Optimistic_Text_Viet_W_Bd.woff2') format('woff2');
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Bd.woff2')
format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;

168
src/utils/compileMDX.ts Normal file
View File

@@ -0,0 +1,168 @@
import {LanguageItem} from 'components/MDX/LanguagesContext';
import {MDXComponents} from 'components/MDX/MDXComponents';
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~
const DISK_CACHE_BREAKER = 10;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export default async function compileMDX(
mdx: string,
path: string | string[],
params: {[key: string]: any}
): Promise<{content: string; toc: string; meta: any}> {
const fs = require('fs');
const {
prepareMDX,
PREPARE_MDX_CACHE_BREAKER,
} = require('../utils/prepareMDX');
const mdxComponentNames = Object.keys(MDXComponents);
// See if we have a cached output first.
const {FileStore, stableHash} = require('metro-cache');
const store = new FileStore({
root: process.cwd() + '/node_modules/.cache/react-docs-mdx/',
});
const hash = Buffer.from(
stableHash({
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~ IMPORTANT: Everything that the code below may rely on.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mdx,
...params,
mdxComponentNames,
DISK_CACHE_BREAKER,
PREPARE_MDX_CACHE_BREAKER,
lockfile: fs.readFileSync(process.cwd() + '/yarn.lock', 'utf8'),
})
);
const cached = await store.get(hash);
if (cached) {
console.log(
'Reading compiled MDX for /' + path + ' from ./node_modules/.cache/'
);
return cached;
}
if (process.env.NODE_ENV === 'production') {
console.log(
'Cache miss for MDX for /' + path + ' from ./node_modules/.cache/'
);
}
// If we don't add these fake imports, the MDX compiler
// will insert a bunch of opaque components we can't introspect.
// This will break the prepareMDX() call below.
let mdxWithFakeImports =
mdx +
'\n\n' +
mdxComponentNames
.map((key) => 'import ' + key + ' from "' + key + '";\n')
.join('\n');
// Turn the MDX we just read into some JS we can execute.
const {remarkPlugins} = require('../../plugins/markdownToHtml');
const {compile: compileMdx} = await import('@mdx-js/mdx');
const visit = (await import('unist-util-visit')).default;
const jsxCode = await compileMdx(mdxWithFakeImports, {
remarkPlugins: [
...remarkPlugins,
(await import('remark-gfm')).default,
(await import('remark-frontmatter')).default,
],
rehypePlugins: [
// Support stuff like ```js App.js {1-5} active by passing it through.
function rehypeMetaAsAttributes() {
return (tree) => {
visit(tree, 'element', (node) => {
if (
// @ts-expect-error -- tagName is a valid property
node.tagName === 'code' &&
node.data &&
node.data.meta
) {
// @ts-expect-error -- properties is a valid property
node.properties.meta = node.data.meta;
}
});
};
},
],
});
const {transform} = require('@babel/core');
const jsCode = await transform(jsxCode, {
plugins: ['@babel/plugin-transform-modules-commonjs'],
presets: ['@babel/preset-react'],
}).code;
// Prepare environment for MDX.
let fakeExports = {};
const fakeRequire = (name: string) => {
if (name === 'react/jsx-runtime') {
return require('react/jsx-runtime');
} else {
// For each fake MDX import, give back the string component name.
// It will get serialized later.
return name;
}
};
const evalJSCode = new Function('require', 'exports', jsCode);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// THIS IS A BUILD-TIME EVAL. NEVER DO THIS WITH UNTRUSTED MDX (LIKE FROM CMS)!!!
// In this case it's okay because anyone who can edit our MDX can also edit this file.
evalJSCode(fakeRequire, fakeExports);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// @ts-expect-error -- default exports is existed after eval
const reactTree = fakeExports.default({});
// Pre-process MDX output and serialize it.
let {toc, children} = prepareMDX(reactTree.props.children);
if (path === 'index') {
toc = [];
}
// Parse Frontmatter headers from MDX.
const fm = require('gray-matter');
const meta = fm(mdx).data;
// Load the list of translated languages conditionally.
let languages: Array<LanguageItem> | null = null;
if (typeof path === 'string' && path.endsWith('/translations')) {
languages = await (
await fetch(
'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'
)
).json(); // { code: string; name: string; enName: string}[]
}
const output = {
content: JSON.stringify(children, stringifyNodeOnServer),
toc: JSON.stringify(toc, stringifyNodeOnServer),
meta,
languages,
};
// Serialize a server React tree node to JSON.
function stringifyNodeOnServer(key: unknown, val: any) {
if (
val != null &&
val.$$typeof === Symbol.for('react.transitional.element')
) {
// Remove fake MDX props.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {mdxType, originalType, parentName, ...cleanProps} = val.props;
return [
'$r',
typeof val.type === 'string' ? val.type : mdxType,
val.key,
cleanProps,
];
} else {
return val;
}
}
// Cache it on the disk.
await store.set(hash, output);
return output;
}

View File

@@ -1,148 +0,0 @@
import fs from 'fs';
import {FileStore, stableHash} from 'metro-cache';
import grayMatter from 'gray-matter';
import {compile, run} from '@mdx-js/mdx';
import * as runtime from 'react/jsx-runtime';
import {remarkPlugins} from '../../plugins/markdownToHtml';
import remarkGfm from 'remark-gfm';
import remarkFrontmatter from 'remark-frontmatter';
import {MDXComponents, MDXComponentsToc} from '../components/MDX/MDXComponents';
import {MaxWidthWrapperPlugin} from './mdx/MaxWidthWrapperPlugin';
import {ExtractedTOC, TOCExtractorPlugin} from './mdx/TOCExtractorPlugin';
import {MetaAttributesPlugin} from './mdx/MetaAttributesPlugin';
const DISK_CACHE_BREAKER = 13;
const CACHE_PATH = `${process.cwd()}/node_modules/.cache/react-docs-mdx/`;
const LOCKFILE_PATH = `${process.cwd()}/yarn.lock`;
type Params = {[key: string]: any};
type MDXResult = {
content: React.ReactNode;
toc: ExtractedTOC[];
meta: any;
};
type CachedResult = {
code: string;
toc: ExtractedTOC[];
meta: any;
};
async function readFromCache(
store: FileStore,
hash: Buffer,
path: string | string[]
): Promise<CachedResult | null> {
try {
const cached = await store.get(hash);
if (cached) {
return JSON.parse(cached.toString());
}
} catch (error) {
console.warn(`Cache read failed for /${path}:`, error);
}
return null;
}
async function writeToCache(
store: FileStore,
hash: Buffer,
result: CachedResult,
path: string | string[]
): Promise<void> {
try {
await store.set(hash, Buffer.from(JSON.stringify(result)));
} catch (error) {
console.warn(`Cache write failed for /${path}:`, error);
}
}
export async function generateMDX(
mdx: string,
path: string | string[],
params: Params
): Promise<MDXResult> {
const store = new FileStore({root: CACHE_PATH});
const hash = Buffer.from(
stableHash({
...params,
mdx,
mdxComponentNames: Object.keys(MDXComponents),
DISK_CACHE_BREAKER,
lockfile: fs.readFileSync(LOCKFILE_PATH, 'utf8'),
})
);
const cachedResult = await readFromCache(store, hash, path);
if (cachedResult) {
const {code, meta, toc} = cachedResult;
const {default: MDXContent} = await run(code, {
...runtime,
baseUrl: import.meta.url,
});
const tocWithMDX = await getTransformedToc(toc);
return {
content: <MDXContent components={{...MDXComponents}} />,
toc: tocWithMDX,
meta,
};
}
const compiled = await compile(mdx, {
remarkPlugins: [
...remarkPlugins,
remarkGfm,
remarkFrontmatter,
MaxWidthWrapperPlugin,
TOCExtractorPlugin,
],
rehypePlugins: [MetaAttributesPlugin],
outputFormat: 'function-body',
});
const {data: meta} = grayMatter(mdx);
const toc = compiled.data.toc as ExtractedTOC[];
const result: CachedResult = {
code: String(compiled),
toc,
meta,
};
await writeToCache(store, hash, result, path);
const tocWithMDX = await getTransformedToc(toc);
const {default: MDXContent} = await run(result.code, {
...runtime,
baseUrl: import.meta.url,
});
return {
content: <MDXContent components={{...MDXComponents}} />,
toc: tocWithMDX,
meta: result.meta,
};
}
async function getTransformedToc(toc: ExtractedTOC[]): Promise<ExtractedTOC[]> {
return await Promise.all(
toc.map(async (item) => {
if (typeof item.node !== 'string') {
return item;
}
const compiled = await compile(item.node, {
outputFormat: 'function-body',
});
const {default: MDXContent} = await run(compiled, {
...runtime,
baseUrl: import.meta.url,
});
item.node = <MDXContent components={{...MDXComponentsToc}} />;
return item;
})
);
}

View File

@@ -1,75 +0,0 @@
import {Metadata} from 'next';
import {siteConfig} from '../siteConfig';
import {finishedTranslations} from 'utils/finishedTranslations';
export interface SeoProps {
title: string;
titleForTitleTag?: string;
description?: string;
image?: string;
isHomePage: boolean;
searchOrder?: number;
path: string;
}
function getDomain(languageCode: string): string {
const subdomain = languageCode === 'en' ? '' : languageCode + '.';
return subdomain + 'react.dev';
}
export function generateMetadata({
title,
titleForTitleTag,
image,
isHomePage,
description: customDescription,
searchOrder,
path,
}: SeoProps): Metadata {
const siteDomain = getDomain(siteConfig.languageCode);
const canonicalUrl = `https://${siteDomain}${path.split(/[\?\#]/)[0]}`;
const pageTitle =
(titleForTitleTag ?? title) + (isHomePage ? '' : ' React');
const twitterTitle = pageTitle.replace(/[<>]/g, '');
const description = isHomePage
? 'React is the library for web and native user interfaces...'
: customDescription ?? 'The library for web and native user interfaces';
const alternateLanguages = {
'x-default': canonicalUrl.replace(siteDomain, getDomain('en')),
...Object.fromEntries(
finishedTranslations.map((languageCode) => [
languageCode,
canonicalUrl.replace(siteDomain, getDomain(languageCode)),
])
),
};
return {
title: pageTitle,
description: isHomePage ? description : undefined,
alternates: {
canonical: canonicalUrl,
languages: alternateLanguages,
},
openGraph: {
title: pageTitle,
description,
url: canonicalUrl,
images: [
{url: `https://${siteDomain}${image || '/images/og-default.png'}`},
],
},
twitter: {
title: twitterTitle,
description,
images: [`https://${siteDomain}${image || '/images/og-default.png'}`],
},
other: {
...(searchOrder != null && {
'algolia-search-order': searchOrder.toString(),
}),
},
};
}

View File

@@ -1,48 +0,0 @@
import {Root, RootContent} from 'mdast';
import {u} from 'unist-builder';
export function MaxWidthWrapperPlugin() {
const fullWidthTypes = [
'Sandpack',
'FullWidth',
'Illustration',
'IllustrationBlock',
'Challenges',
'Recipes',
];
return (tree: Root) => {
const newChildren: RootContent[] = [];
let wrapQueue: RootContent[] = [];
function flushWrapper() {
if (wrapQueue.length > 0) {
newChildren.push(
u('mdxJsxFlowElement', {
name: 'MaxWidth',
attributes: [],
children: wrapQueue,
} as any)
);
wrapQueue = [];
}
}
for (const node of tree.children) {
if (
// @ts-expect-error
fullWidthTypes.includes(node.name) &&
// @ts-expect-error: mdxJsxFlowElement
node.type === 'mdxJsxFlowElement'
) {
flushWrapper();
newChildren.push(node);
} else {
wrapQueue.push(node);
}
}
flushWrapper();
tree.children = newChildren;
};
}

View File

@@ -1,19 +0,0 @@
import {Root} from 'mdast';
import visit from 'unist-util-visit';
// Support stuff like ```js App.js {1-5} active by passing it through.
export function MetaAttributesPlugin() {
return (tree: Root) => {
visit(tree, 'element', (node) => {
if (
// @ts-expect-error -- tagName is a valid property
node.tagName === 'code' &&
node.data &&
node.data.meta
) {
// @ts-expect-error -- properties is a valid property
node.properties.meta = node.data.meta;
}
});
};
}

View File

@@ -1,131 +0,0 @@
import visit from 'unist-util-visit';
import {Node} from 'unist';
interface HeadingNode extends Node {
type: 'heading';
depth: number;
children: Array<{
type: string;
value?: string;
}>;
data?: {
hProperties?: {
id?: string;
};
};
}
interface MDXJsxFlowElementNode extends Node {
type: 'mdxJsxFlowElement';
name: string;
attributes?: Array<{
name: string;
value?: string;
}>;
}
export interface ExtractedTOC {
url: string;
node: string | React.ReactNode;
depth: number;
}
interface PluginOptions {
maxDepth?: number;
}
export function TOCExtractorPlugin({maxDepth = 3}: PluginOptions = {}) {
return (tree: Node, file: any) => {
const toc: ExtractedTOC[] = [];
visit(tree, (node: Node) => {
// Standard markdown headings
if (node.type === 'heading') {
const headingNode = node as HeadingNode;
if (headingNode.depth > maxDepth) {
return;
}
const mdxSource = file.value
.slice(
// @ts-ignore
node.children[0].position!.start.offset,
// @ts-ignore
node.children[0].position!.end.offset
)
.trim();
const text = headingNode.children
.filter((child) => child.type === 'text' && child.value)
.map((child) => child.value!)
.join('');
const id =
headingNode.data?.hProperties?.id ||
text.toLowerCase().replace(/\s+/g, '-');
toc.push({
depth: headingNode.depth,
node: mdxSource,
url: `#${id}`,
});
}
// MDX custom components (e.g., <TeamMember>)
else if (node.type === 'mdxJsxFlowElement') {
const mdxNode = node as MDXJsxFlowElementNode;
switch (mdxNode.name) {
case 'TeamMember': {
let name = 'Team Member';
let permalink = 'team-member';
if (Array.isArray(mdxNode.attributes)) {
for (const attr of mdxNode.attributes) {
if (attr.name === 'name' && attr.value) {
name = attr.value;
} else if (attr.name === 'permalink' && attr.value) {
permalink = attr.value;
}
}
}
toc.push({
url: `#${permalink}`,
depth: 3,
node: name,
});
break;
}
case 'Challenges':
toc.push({
url: '#challenges',
depth: 2,
node: 'Challenges',
});
break;
case 'Recap':
toc.push({
url: '#recap',
depth: 2,
node: 'Recap',
});
break;
default:
break;
}
}
});
// Insert "Overview" at the top if there's at least one heading
if (toc.length > 0) {
toc.unshift({
url: '#',
node: 'Overview',
depth: 2,
});
}
file.data.toc = toc;
};
}

117
src/utils/prepareMDX.js Normal file
View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {Children} from 'react';
// TODO: This logic could be in MDX plugins instead.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export const PREPARE_MDX_CACHE_BREAKER = 3;
// !!! IMPORTANT !!! Bump this if you change any logic.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export function prepareMDX(rawChildren) {
const toc = getTableOfContents(rawChildren, /* depth */ 10);
const children = wrapChildrenInMaxWidthContainers(rawChildren);
return {toc, children};
}
function wrapChildrenInMaxWidthContainers(children) {
// Auto-wrap everything except a few types into
// <MaxWidth> wrappers. Keep reusing the same
// wrapper as long as we can until we meet
// a full-width section which interrupts it.
let fullWidthTypes = [
'Sandpack',
'FullWidth',
'Illustration',
'IllustrationBlock',
'Challenges',
'Recipes',
];
let wrapQueue = [];
let finalChildren = [];
function flushWrapper(key) {
if (wrapQueue.length > 0) {
const Wrapper = 'MaxWidth';
finalChildren.push(<Wrapper key={key}>{wrapQueue}</Wrapper>);
wrapQueue = [];
}
}
function handleChild(child, key) {
if (child == null) {
return;
}
if (typeof child !== 'object') {
wrapQueue.push(child);
return;
}
if (fullWidthTypes.includes(child.type)) {
flushWrapper(key);
finalChildren.push(child);
} else {
wrapQueue.push(child);
}
}
Children.forEach(children, handleChild);
flushWrapper('last');
return finalChildren;
}
function getTableOfContents(children, depth) {
const anchors = [];
extractHeaders(children, depth, anchors);
if (anchors.length > 0) {
anchors.unshift({
url: '#',
text: 'Overview',
depth: 2,
});
}
return anchors;
}
const headerTypes = new Set([
'h1',
'h2',
'h3',
'Challenges',
'Recap',
'TeamMember',
]);
function extractHeaders(children, depth, out) {
for (const child of Children.toArray(children)) {
if (child.type && headerTypes.has(child.type)) {
let header;
if (child.type === 'Challenges') {
header = {
url: '#challenges',
depth: 2,
text: 'Challenges',
};
} else if (child.type === 'Recap') {
header = {
url: '#recap',
depth: 2,
text: 'Recap',
};
} else if (child.type === 'TeamMember') {
header = {
url: '#' + child.props.permalink,
depth: 3,
text: child.props.name,
};
} else {
header = {
url: '#' + child.props.id,
depth: (child.type && parseInt(child.type.replace('h', ''), 0)) ?? 0,
text: child.props.children,
};
}
out.push(header);
} else if (child.children && depth > 0) {
extractHeaders(child.children, depth - 1, out);
}
}
}

82
src/utils/rss.js Normal file
View File

@@ -0,0 +1,82 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
const Feed = require('rss');
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const getAllFiles = function (dirPath, arrayOfFiles) {
const files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];
files.forEach(function (file) {
if (fs.statSync(dirPath + '/' + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles);
} else {
arrayOfFiles.push(path.join(dirPath, '/', file));
}
});
return arrayOfFiles;
};
exports.generateRssFeed = function () {
const feed = new Feed({
title: 'React Blog',
description:
'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.',
feed_url: 'https://react.dev/rss.xml',
site_url: 'https://react.dev/',
language: 'en',
favicon: 'https://react.dev/favicon.ico',
pubDate: new Date(),
generator: 'react.dev rss module',
});
const dirPath = path.join(process.cwd(), 'src/content/blog');
const filesByOldest = getAllFiles(dirPath);
const files = filesByOldest.reverse();
for (const filePath of files) {
const id = path.basename(filePath);
if (id !== 'index.md') {
const content = fs.readFileSync(filePath, 'utf-8');
const {data} = matter(content);
const slug = filePath.split('/').slice(-4).join('/').replace('.md', '');
if (data.title == null || data.title.trim() === '') {
throw new Error(
`${id}: Blog posts must include a title in the metadata, for RSS feeds`
);
}
if (data.author == null || data.author.trim() === '') {
throw new Error(
`${id}: Blog posts must include an author in the metadata, for RSS feeds`
);
}
if (data.date == null || data.date.trim() === '') {
throw new Error(
`${id}: Blog posts must include a date in the metadata, for RSS feeds`
);
}
if (data.description == null || data.description.trim() === '') {
throw new Error(
`${id}: Blog posts must include a description in the metadata, for RSS feeds`
);
}
feed.item({
id,
title: data.title,
author: data.author || '',
date: new Date(data.date),
url: `https://react.dev/blog/${slug}`,
description: data.description,
});
}
}
fs.writeFileSync('./public/rss.xml', feed.xml({indent: true}));
};

View File

@@ -9,7 +9,6 @@ module.exports = {
content: [
'./src/components/**/*.{js,ts,jsx,tsx}',
'./src/pages/**/*.{js,ts,jsx,tsx}',
'./src/app/**/*.{js,ts,jsx,tsx}',
'./src/styles/**/*.{js,ts,jsx,tsx}',
],
darkMode: 'class',

View File

@@ -1,12 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": false,
"noImplicitReturns": true,
"noImplicitThis": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
@@ -18,20 +22,14 @@
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "src",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
"incremental": true
},
"include": [
"next-env.d.ts",
"src/**/*.ts",
"src/**/*.tsx",
".next/types/**/*.ts",
"types.d.ts",
"src/components/UwuScript.jsx"
"src/**/*.tsx"
],
"exclude": ["node_modules"]
"exclude": [
"node_modules"
]
}

8
types.d.ts vendored
View File

@@ -1,8 +0,0 @@
declare module 'metro-cache' {
export class FileStore {
constructor(options: {root: string});
get(hash: Buffer): Promise<Buffer | null>;
set(hash: Buffer, value: Buffer): Promise<void>;
}
export function stableHash(obj: any): string;
}

142
yarn.lock
View File

@@ -1225,10 +1225,10 @@
unist-util-visit "^4.0.0"
vfile "^5.0.0"
"@next/env@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0-canary.33.tgz#5cd769cca64e09564e80817b6b6aeaba472ea9e9"
integrity sha512-y3EPM+JYKU8t2K+i6bc0QrotEZVGpqu9eVjprj4cfS8QZyZcL54s+W9aGB0TBuGavU9tQdZ50W186+toeMV+hw==
"@next/env@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.0.tgz#35b00a5f60ff10dc275182928c325d25c29379ae"
integrity sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==
"@next/eslint-plugin-next@12.0.3":
version "12.0.3"
@@ -1237,45 +1237,45 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0-canary.33.tgz#946b9fa766575baf7577ea21b70105d5fefc86ed"
integrity sha512-+fCdK2KmR6lWoCTk1fSd5pvbiLZHfZF+D/Xdz3xrXw+pbnBtXWLKQrPT0bCtDseMxD31qcOywq5mAApvI3EGpA==
"@next/swc-darwin-arm64@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz#30cb89220e719244c9fa7391641e515a078ade46"
integrity sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==
"@next/swc-darwin-x64@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0-canary.33.tgz#8878b319cd3d3f90648097d5d76b54b1db02d4f0"
integrity sha512-GrrU+tSmeBRow+7bnn7i5M96g3tc28hPH5t5Y65qUXGmmrZwGZN1e1d+8QbXPdAGkvjEPcOkUNQuQVpp1qpYPA==
"@next/swc-darwin-x64@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz#c24c4f5d1016dd161da32049305b0ddddfc80951"
integrity sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==
"@next/swc-linux-arm64-gnu@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0-canary.33.tgz#0bc4f61ccbb4e7424f9acb215f0d7e0674456f5e"
integrity sha512-8RnGxnUpASHoUf6aHUifmZom5b4Ow5nTdCib/CNYXZ6VLuL5ocvmr+DXs/SKzi9h8OHR7JkLwKXHCcF8WyscSg==
"@next/swc-linux-arm64-gnu@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz#08ed540ecdac74426a624cc7d736dc709244b004"
integrity sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==
"@next/swc-linux-arm64-musl@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0-canary.33.tgz#15fa3982f382dc6c51ac17d47111b6525deb7f94"
integrity sha512-COyE0LzMuLBZSR+Z/TOGilyJPdwSU588Vt0+o8GoECkoDEnjyuO2s2nHa2kDAcEfUEPkhlo0tErU3mF+8AVOTQ==
"@next/swc-linux-arm64-musl@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz#dfddbd40087d018266aa92515ec5b3e251efa6dd"
integrity sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==
"@next/swc-linux-x64-gnu@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0-canary.33.tgz#94d06dcb6f116c470d9d6a7ddda0a29ab23ce80b"
integrity sha512-3Y9lqJs+ftU9jgbLdCtvAvF8MNJsJYGMH7icb8QMs1+yOyHHbmwkZoElKdjwfUWzQ2sX28ywp73GWq4HbrsoUg==
"@next/swc-linux-x64-gnu@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz#a7b5373a1b28c0acecbc826a3790139fc0d899e5"
integrity sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==
"@next/swc-linux-x64-musl@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0-canary.33.tgz#daaa3610648afe7440f3672d11352ecd5109d2df"
integrity sha512-FS9iA+RkZlhdWGQEKtsplVBXIYZJUn5nsRB+1UY46b3uaL6dDypu13ODaSwYuAwXGgkrZBVF9AFO3y4biBnPlA==
"@next/swc-linux-x64-musl@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz#b82a29903ee2f12d8b64163ddf208ac519869550"
integrity sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==
"@next/swc-win32-arm64-msvc@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0-canary.33.tgz#e704113f42449f30ee0d81337ff9c27177cad75c"
integrity sha512-Ji9CtBbUx06qvvN/rPohJN2FEFGsUv26F50f2nMRYRwrq3POXDjloGOiRocrjU0ty/cUzCz71qTUfKdmv/ajmg==
"@next/swc-win32-arm64-msvc@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz#98deae6cb1fccfb6a600e9faa6aa714402a9ab9a"
integrity sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==
"@next/swc-win32-x64-msvc@15.2.0-canary.33":
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0-canary.33.tgz#d719ea46ba61f3a97678dd0b290147488ea974ba"
integrity sha512-hjdbGnkwIZ8zN2vlS6lNsEJO37HRtcEGimzfkruBMsi/DwJBqkJvZbNC/XCJy3HFcU58igncqV52p1IPjmAJAw==
"@next/swc-win32-x64-msvc@15.1.0":
version "15.1.0"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz#4b04a6a667c41fecdc63db57dd71ca7e84d0946b"
integrity sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1656,13 +1656,6 @@
dependencies:
"@types/unist" "*"
"@types/mdast@^4.0.4":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
dependencies:
"@types/unist" "*"
"@types/mdurl@^1.0.0":
version "1.0.2"
resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz"
@@ -1731,11 +1724,6 @@
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@typescript-eslint/eslint-plugin@^5.36.2":
version "5.36.2"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz"
@@ -2435,10 +2423,15 @@ camelcase-css@^2.0.1:
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688:
version "1.0.30001695"
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz"
integrity sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001579:
version "1.0.30001636"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78"
integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==
caniuse-lite@^1.0.30001688:
version "1.0.30001692"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz#4585729d95e6b95be5b439da6ab55250cd125bf9"
integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
ccount@^1.0.0:
version "1.1.0"
@@ -2522,13 +2515,6 @@ chokidar@^3.4.0, chokidar@^3.5.3:
optionalDependencies:
fsevents "~2.3.2"
chokidar@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
ci-info@^3.2.0:
version "3.3.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz"
@@ -5812,12 +5798,12 @@ next-tick@^1.1.0:
resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
next@^15.2.0-canary.33:
version "15.2.0-canary.33"
resolved "https://registry.yarnpkg.com/next/-/next-15.2.0-canary.33.tgz#a866edb669618a5a7aac5035895255eb0c9fb189"
integrity sha512-WF8QLeYkakuYwksdWY/F+Bi8tNJfIbiSYk9hCmldn9sNp1lU3lqI1hrW1ynbcMSaXC+qQEr7yol2OdvVZ4nZYQ==
next@15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/next/-/next-15.1.0.tgz#be847cf67ac94ae23b57f3ea6d10642f3fc1ad69"
integrity sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==
dependencies:
"@next/env" "15.2.0-canary.33"
"@next/env" "15.1.0"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
@@ -5825,14 +5811,14 @@ next@^15.2.0-canary.33:
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.2.0-canary.33"
"@next/swc-darwin-x64" "15.2.0-canary.33"
"@next/swc-linux-arm64-gnu" "15.2.0-canary.33"
"@next/swc-linux-arm64-musl" "15.2.0-canary.33"
"@next/swc-linux-x64-gnu" "15.2.0-canary.33"
"@next/swc-linux-x64-musl" "15.2.0-canary.33"
"@next/swc-win32-arm64-msvc" "15.2.0-canary.33"
"@next/swc-win32-x64-msvc" "15.2.0-canary.33"
"@next/swc-darwin-arm64" "15.1.0"
"@next/swc-darwin-x64" "15.1.0"
"@next/swc-linux-arm64-gnu" "15.1.0"
"@next/swc-linux-arm64-musl" "15.1.0"
"@next/swc-linux-x64-gnu" "15.1.0"
"@next/swc-linux-x64-musl" "15.1.0"
"@next/swc-win32-arm64-msvc" "15.1.0"
"@next/swc-win32-x64-msvc" "15.1.0"
sharp "^0.33.5"
nice-try@^1.0.4:
@@ -6814,11 +6800,6 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
path-type "^3.0.0"
readdirp@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.1.tgz#bd115327129672dc47f87408f05df9bd9ca3ef55"
integrity sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
@@ -8106,13 +8087,6 @@ unist-builder@^3.0.0:
dependencies:
"@types/unist" "^2.0.0"
unist-builder@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-4.0.0.tgz#817b326c015a6f9f5e92bb55b8e8bc5e578fe243"
integrity sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==
dependencies:
"@types/unist" "^3.0.0"
unist-util-generated@^1.0.0:
version "1.1.6"
resolved "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz"