From a5069837be74460fccf399d357c756d9c173283a Mon Sep 17 00:00:00 2001
From: Igor Shevchenko <39371503+bnzone@users.noreply.github.com>
Date: Mon, 30 Sep 2024 11:00:08 -0500
Subject: [PATCH 002/109] chore(docs): fix typo (#7194)
---
src/content/reference/react-dom/preinitModule.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/reference/react-dom/preinitModule.md b/src/content/reference/react-dom/preinitModule.md
index 996c5a2ed..5f9be6d50 100644
--- a/src/content/reference/react-dom/preinitModule.md
+++ b/src/content/reference/react-dom/preinitModule.md
@@ -51,7 +51,7 @@ The `preinitModule` function provides the browser with a hint that it should sta
#### Parameters {/*parameters*/}
-* `href`: a string. The URL of the module you want to download and exeucute.
+* `href`: a string. The URL of the module you want to download and execute.
* `options`: an object. It contains the following properties:
* `as`: a required string. It must be `'script'`.
* `crossOrigin`: a string. The [CORS policy](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) to use. Its possible values are `anonymous` and `use-credentials`.
From 8a62ce3a12a99bdc4967150b5e18e59cd3aa82c1 Mon Sep 17 00:00:00 2001
From: Prem Singh <134128887+premdood@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:30:38 +0530
Subject: [PATCH 003/109] Add files via upload (#7182)
---
src/content/learn/synchronizing-with-effects.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/learn/synchronizing-with-effects.md b/src/content/learn/synchronizing-with-effects.md
index 48e99cc27..115075161 100644
--- a/src/content/learn/synchronizing-with-effects.md
+++ b/src/content/learn/synchronizing-with-effects.md
@@ -627,7 +627,7 @@ See the examples below for how to handle common patterns.
### Controlling non-React widgets {/*controlling-non-react-widgets*/}
-Sometimes you need to add UI widgets that aren't written to React. For example, let's say you're adding a map component to your page. It has a `setZoomLevel()` method, and you'd like to keep the zoom level in sync with a `zoomLevel` state variable in your React code. Your Effect would look similar to this:
+Sometimes you need to add UI widgets that aren't written in React. For example, let's say you're adding a map component to your page. It has a `setZoomLevel()` method, and you'd like to keep the zoom level in sync with a `zoomLevel` state variable in your React code. Your Effect would look similar to this:
```js
useEffect(() => {
From ae9726a8532b99ed4a2c40946dfa96a82f8a12ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rado=C5=A1=20Mili=C4=87ev?=
<40705899+rammba@users.noreply.github.com>
Date: Mon, 30 Sep 2024 18:07:07 +0200
Subject: [PATCH 004/109] Docs: Fix typos in thinking-in-react.md (#7179)
* Fix typos in thinking-in-react.md
* Update src/content/learn/thinking-in-react.md
---------
Co-authored-by: Ricky
---
src/content/learn/thinking-in-react.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/content/learn/thinking-in-react.md b/src/content/learn/thinking-in-react.md
index 0f05a0569..822891e60 100644
--- a/src/content/learn/thinking-in-react.md
+++ b/src/content/learn/thinking-in-react.md
@@ -268,8 +268,8 @@ Now let's run through our strategy for them:
1. **Identify components that use state:**
* `ProductTable` needs to filter the product list based on that state (search text and checkbox value).
* `SearchBar` needs to display that state (search text and checkbox value).
-1. **Find their common parent:** The first parent component both components share is `FilterableProductTable`.
-2. **Decide where the state lives**: We'll keep the filter text and checked state values in `FilterableProductTable`.
+2. **Find their common parent:** The first parent component both components share is `FilterableProductTable`.
+3. **Decide where the state lives**: We'll keep the filter text and checked state values in `FilterableProductTable`.
So the state values will live in `FilterableProductTable`.
From 6d2f3373d178a4f00b50ae201f0f2855cb468906 Mon Sep 17 00:00:00 2001
From: Mo Javad <34189022+mojavad@users.noreply.github.com>
Date: Mon, 30 Sep 2024 17:21:44 +0100
Subject: [PATCH 005/109] Add React Native London Conf to the community
conferences (#7173)
---
src/content/community/conferences.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/content/community/conferences.md b/src/content/community/conferences.md
index 297294db0..aaa761218 100644
--- a/src/content/community/conferences.md
+++ b/src/content/community/conferences.md
@@ -45,6 +45,11 @@ October 25 & 28, 2024. In-person in London, UK + online (hybrid event)
[Website](https://reactadvanced.com/) - [Twitter](https://x.com/reactadvanced)
+### React Native London Conf 2024 {/*react-native-london-2024*/}
+November 14 & 15, 2024. In-person in London, UK
+
+[Website](https://reactnativelondon.co.uk/) - [Twitter](https://x.com/RNLConf)
+
### React Summit US 2024 {/*react-summit-us-2024*/}
November 19 & 22, 2024. In-person in New York, USA + online (hybrid event)
From d418485aedb956bb98ea6f39afc769d10c82e625 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Parth=20=E2=80=A2=20imParth?=
<108121667+imparth7@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:55:38 +0530
Subject: [PATCH 006/109] fix:#7158 issue (#7159)
---
src/utils/rss.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/utils/rss.js b/src/utils/rss.js
index c6fb82410..29e5511ea 100644
--- a/src/utils/rss.js
+++ b/src/utils/rss.js
@@ -40,7 +40,7 @@ exports.generateRssFeed = function () {
const files = filesByOldest.reverse();
for (const filePath of files) {
- const id = filePath.split('/').slice(-1).join('');
+ const id = path.basename(filePath);
if (id !== 'index.md') {
const content = fs.readFileSync(filePath, 'utf-8');
const {data} = matter(content);
From c7392cbcc1d7dcefccd649b6485a5775271bded8 Mon Sep 17 00:00:00 2001
From: Reg Chiu
Date: Tue, 1 Oct 2024 00:54:54 +0800
Subject: [PATCH 007/109] docs(act.md): correct ReactDOM to ReactDOMClient
(#7156)
* docs(act.md): correct ReactDOMClient to ReactDOM
* docs(act.md): switch the import to ReactDOMClient instead
---
src/content/reference/react/act.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/content/reference/react/act.md b/src/content/reference/react/act.md
index 256befa31..c01f3cd69 100644
--- a/src/content/reference/react/act.md
+++ b/src/content/reference/react/act.md
@@ -90,7 +90,7 @@ To test the render output of a component, wrap the render inside `act()`:
```js {10,12}
import {act} from 'react';
-import ReactDOM from 'react-dom/client';
+import ReactDOMClient from 'react-dom/client';
import Counter from './Counter';
it('can render and update a counter', async () => {
@@ -99,7 +99,7 @@ it('can render and update a counter', async () => {
// ✅ Render the component inside act().
await act(() => {
- ReactDOM.createRoot(container).render( );
+ ReactDOMClient.createRoot(container).render( );
});
const button = container.querySelector('button');
@@ -119,7 +119,7 @@ To test events, wrap the event dispatch inside `act()`:
```js {14,16}
import {act} from 'react';
-import ReactDOM from 'react-dom/client';
+import ReactDOMClient from 'react-dom/client';
import Counter from './Counter';
it.only('can render and update a counter', async () => {
@@ -174,4 +174,4 @@ global.IS_REACT_ACT_ENVIRONMENT=true
In testing frameworks like [React Testing Library](https://testing-library.com/docs/react-testing-library/intro), `IS_REACT_ACT_ENVIRONMENT` is already set for you.
-
\ No newline at end of file
+
From 44364225ac337d84ecc62b8e8150d0c5c859cda5 Mon Sep 17 00:00:00 2001
From: Steven SAN <49305087+ssan93@users.noreply.github.com>
Date: Tue, 1 Oct 2024 02:05:12 +0900
Subject: [PATCH 008/109] chore(typo): Fix comment to match code (#7147)
Co-authored-by: san.s
---
src/components/MDX/Sandpack/Preview.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/MDX/Sandpack/Preview.tsx b/src/components/MDX/Sandpack/Preview.tsx
index 9669e5f4f..7d7cdc5a7 100644
--- a/src/components/MDX/Sandpack/Preview.tsx
+++ b/src/components/MDX/Sandpack/Preview.tsx
@@ -113,7 +113,7 @@ export function Preview({
/**
* The spinner component transition might be longer than
* the bundler loading, so we only show the spinner if
- * it takes more than 1s to load the bundler.
+ * it takes more than 500s to load the bundler.
*/
timeout = setTimeout(() => {
setShowLoading(true);
From 7a8e256c898efec62914222ccf38ba4c3a6c560e Mon Sep 17 00:00:00 2001
From: Faris P
Date: Mon, 30 Sep 2024 22:39:24 +0530
Subject: [PATCH 009/109] Fix minor grammar issue in tutorial-tic-tac-toe.md
(`a` to `an`) (#7136)
---
src/content/learn/tutorial-tic-tac-toe.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/learn/tutorial-tic-tac-toe.md b/src/content/learn/tutorial-tic-tac-toe.md
index f18ec4939..6487e8007 100644
--- a/src/content/learn/tutorial-tic-tac-toe.md
+++ b/src/content/learn/tutorial-tic-tac-toe.md
@@ -1133,7 +1133,7 @@ Calling the `setSquares` function lets React know the state of the component has
-JavaScript supports [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) which means an inner function (e.g. `handleClick`) has access to variables and functions defined in a outer function (e.g. `Board`). The `handleClick` function can read the `squares` state and call the `setSquares` method because they are both defined inside of the `Board` function.
+JavaScript supports [closures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures) which means an inner function (e.g. `handleClick`) has access to variables and functions defined in an outer function (e.g. `Board`). The `handleClick` function can read the `squares` state and call the `setSquares` method because they are both defined inside of the `Board` function.
From 4fe9c85bc0a9296e1ec152210bc4446d026626c7 Mon Sep 17 00:00:00 2001
From: youngvform
Date: Tue, 1 Oct 2024 02:12:09 +0900
Subject: [PATCH 010/109] remove wrong reference of window (#7132)
---
.../reference/rules/components-and-hooks-must-be-pure.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/reference/rules/components-and-hooks-must-be-pure.md b/src/content/reference/rules/components-and-hooks-must-be-pure.md
index 733597c63..9da65d04a 100644
--- a/src/content/reference/rules/components-and-hooks-must-be-pure.md
+++ b/src/content/reference/rules/components-and-hooks-must-be-pure.md
@@ -190,7 +190,7 @@ Side effects that are directly visible to the user are not allowed in the render
```js {2}
function ProductDetailPage({ product }) {
- document.window.title = product.title; // 🔴 Bad: Changes the DOM
+ document.title = product.title; // 🔴 Bad: Changes the DOM
}
```
From 8fee25f4ec2bf87749ad82ae1f6b7ed4d0e8b138 Mon Sep 17 00:00:00 2001
From: Muhammad Saqib
Date: Mon, 30 Sep 2024 22:14:50 +0500
Subject: [PATCH 011/109] Update renderToPipeableStream.md (#7131)
---
.../reference/react-dom/server/renderToPipeableStream.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/content/reference/react-dom/server/renderToPipeableStream.md b/src/content/reference/react-dom/server/renderToPipeableStream.md
index 20a5960eb..7d0d1ab3d 100644
--- a/src/content/reference/react-dom/server/renderToPipeableStream.md
+++ b/src/content/reference/react-dom/server/renderToPipeableStream.md
@@ -57,7 +57,7 @@ On the client, call [`hydrateRoot`](/reference/react-dom/client/hydrateRoot) to
* **optional** `nonce`: A [`nonce`](http://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#nonce) string to allow scripts for [`script-src` Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src).
* **optional** `onAllReady`: A callback that fires when all rendering is complete, including both the [shell](#specifying-what-goes-into-the-shell) and all additional [content.](#streaming-more-content-as-it-loads) You can use this instead of `onShellReady` [for crawlers and static generation.](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) If you start streaming here, you won't get any progressive loading. The stream will contain the final HTML.
* **optional** `onError`: A callback that fires whenever there is a server error, whether [recoverable](#recovering-from-errors-outside-the-shell) or [not.](#recovering-from-errors-inside-the-shell) By default, this only calls `console.error`. If you override it to [log crash reports,](#logging-crashes-on-the-server) make sure that you still call `console.error`. You can also use it to [adjust the status code](#setting-the-status-code) before the shell is emitted.
- * **optional** `onShellReady`: A callback that fires right after the [initial shell](#specifying-what-goes-into-the-shell) has been rendered. You can [set the status code](#setting-the-status-code) and call `pipe` here to start streaming. React will [stream the additional content](#streaming-more-content-as-it-loads) after the shell along with the inline `
+```
+
+On the client, your bootstrap script should [hydrate the entire `document` with a call to `hydrateRoot`:](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document)
+
+```js [[1, 4, " "]]
+import { hydrateRoot } from 'react-dom/client';
+import App from './App.js';
+
+hydrateRoot(document, );
+```
+
+This will attach event listeners to the static server-generated HTML and make it interactive.
+
+
+
+#### Reading CSS and JS asset paths from the build output {/*reading-css-and-js-asset-paths-from-the-build-output*/}
+
+The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of `styles.css` you might end up with `styles.123456.css`. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.
+
+However, if you don't know the asset URLs until after the build, there's no way for you to put them in the source code. For example, hardcoding `"/styles.css"` into JSX like earlier wouldn't work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop:
+
+```js {1,6}
+export default function App({ assetMap }) {
+ return (
+
+
+ My app
+
+
+ ...
+
+ );
+}
+```
+
+On the server, render ` ` and pass your `assetMap` with the asset URLs:
+
+```js {1-5,8,9}
+// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
+const assetMap = {
+ 'styles.css': '/styles.123456.css',
+ 'main.js': '/main.123456.js'
+};
+
+async function handler(request) {
+ const {prelude} = await prerender( , {
+ bootstrapScripts: [assetMap['/main.js']]
+ });
+ return new Response(prelude, {
+ headers: { 'content-type': 'text/html' },
+ });
+}
+```
+
+Since your server is now rendering ` `, you need to render it with `assetMap` on the client too to avoid hydration errors. You can serialize and pass `assetMap` to the client like this:
+
+```js {9-10}
+// You'd need to get this JSON from your build tooling.
+const assetMap = {
+ 'styles.css': '/styles.123456.css',
+ 'main.js': '/main.123456.js'
+};
+
+async function handler(request) {
+ const {prelude} = await prerender( , {
+ // Careful: It's safe to stringify() this because this data isn't user-generated.
+ bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
+ bootstrapScripts: [assetMap['/main.js']],
+ });
+ return new Response(prelude, {
+ headers: { 'content-type': 'text/html' },
+ });
+}
+```
+
+In the example above, the `bootstrapScriptContent` option adds an extra inline `
+```
+
+On the client, your bootstrap script should [hydrate the entire `document` with a call to `hydrateRoot`:](/reference/react-dom/client/hydrateRoot#hydrating-an-entire-document)
+
+```js [[1, 4, " "]]
+import { hydrateRoot } from 'react-dom/client';
+import App from './App.js';
+
+hydrateRoot(document, );
+```
+
+This will attach event listeners to the static server-generated HTML and make it interactive.
+
+
+
+#### Reading CSS and JS asset paths from the build output {/*reading-css-and-js-asset-paths-from-the-build-output*/}
+
+The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of `styles.css` you might end up with `styles.123456.css`. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.
+
+However, if you don't know the asset URLs until after the build, there's no way for you to put them in the source code. For example, hardcoding `"/styles.css"` into JSX like earlier wouldn't work. To keep them out of your source code, your root component can read the real filenames from a map passed as a prop:
+
+```js {1,6}
+export default function App({ assetMap }) {
+ return (
+
+
+ My app
+
+
+ ...
+
+ );
+}
+```
+
+On the server, render ` ` and pass your `assetMap` with the asset URLs:
+
+```js {1-5,8,9}
+// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
+const assetMap = {
+ 'styles.css': '/styles.123456.css',
+ 'main.js': '/main.123456.js'
+};
+
+app.use('/', async (request, response) => {
+ const { prelude } = await prerenderToNodeStream( , {
+ bootstrapScripts: [assetMap['/main.js']]
+ });
+
+ response.setHeader('Content-Type', 'text/html');
+ prelude.pipe(response);
+});
+```
+
+Since your server is now rendering ` `, you need to render it with `assetMap` on the client too to avoid hydration errors. You can serialize and pass `assetMap` to the client like this:
+
+```js {9-10}
+// You'd need to get this JSON from your build tooling.
+const assetMap = {
+ 'styles.css': '/styles.123456.css',
+ 'main.js': '/main.123456.js'
+};
+
+app.use('/', async (request, response) => {
+ const { prelude } = await prerenderToNodeStream( , {
+ // Careful: It's safe to stringify() this because this data isn't user-generated.
+ bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
+ bootstrapScripts: [assetMap['/main.js']],
+ });
+
+ response.setHeader('Content-Type', 'text/html');
+ prelude.pipe(response);
+});
+```
+
+In the example above, the `bootstrapScriptContent` option adds an extra inline `
+ >
+ );
+}
diff --git a/src/components/DevContentRefresher.tsx b/src/components/DevContentRefresher.tsx
new file mode 100644
index 000000000..31a0961ed
--- /dev/null
+++ b/src/components/DevContentRefresher.tsx
@@ -0,0 +1,29 @@
+'use client';
+
+import {useRouter} from 'next/navigation';
+import {useRef, useEffect} from 'react';
+
+export function DevContentRefresher() {
+ const router = useRouter();
+ const wsRef = useRef(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;
+}
diff --git a/src/components/ErrorDecoderProvider.tsx b/src/components/ErrorDecoderProvider.tsx
new file mode 100644
index 000000000..bad1ed2d0
--- /dev/null
+++ b/src/components/ErrorDecoderProvider.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import {ErrorDecoderContext} from './ErrorDecoderContext';
+
+export function ErrorDecoderProvider({
+ children,
+ errorMessage,
+ errorCode,
+}: {
+ children: React.ReactNode;
+ errorMessage: string | null;
+ errorCode: string | null;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx
index 34db728ce..16b974c10 100644
--- a/src/components/Layout/Feedback.tsx
+++ b/src/components/Layout/Feedback.tsx
@@ -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 {asPath} = useRouter();
- const cleanedPath = asPath.split(/[\?\#]/)[0];
+ const pathname = usePathname();
+ const cleanedPath = pathname.split(/[\?\#]/)[0];
// Reset on route changes.
return ;
}
diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx
index 24d379589..ea0c53e1c 100644
--- a/src/components/Layout/Page.tsx
+++ b/src/components/Layout/Page.tsx
@@ -1,16 +1,16 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
-import {Suspense} from 'react';
import * as React from 'react';
-import {useRouter} from 'next/router';
+import {Suspense} from 'react';
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,6 +36,7 @@ interface PageProps {
};
section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown';
languages?: Languages | null;
+ pathname: string;
}
export function Page({
@@ -44,11 +45,11 @@ export function Page({
routeTree,
meta,
section,
+ pathname,
languages = null,
}: PageProps) {
- const {asPath} = useRouter();
- const cleanedPath = asPath.split(/[\?\#]/)[0];
- const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta(
+ const cleanedPath = pathname.split(/[\?\#]/)[0];
+ const {route, nextRoute, prevRoute, breadcrumbs} = getRouteMeta(
cleanedPath,
routeTree
);
@@ -113,31 +114,17 @@ export function Page({
showSidebar = false;
}
- let searchOrder;
- if (section === 'learn' || (section === 'blog' && !isBlogIndex)) {
- searchOrder = order;
- }
-
return (
<>
-
{(isHomePage || isBlogIndex) && (
-
-
-
+ // RSS Feed link is now handled by metadata in layout.tsx
+
)}
- {/* */}
-
+
{content}
- {showToc && toc.length > 0 && }
+ {showToc && toc.length > 0 && }
>
diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx
index 72003df74..f67b0ed2b 100644
--- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx
+++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx
@@ -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 = useRouter().asPath.split(/[\?\#]/)[0];
+ const slug = usePathname().split(/[\?\#]/)[0];
const pendingRoute = usePendingRoute();
const currentRoutes = routeTree.routes as RouteItem[];
return (
diff --git a/src/components/Layout/Toc.tsx b/src/components/Layout/Toc.tsx
index 5308c602c..a8d269898 100644
--- a/src/components/Layout/Toc.tsx
+++ b/src/components/Layout/Toc.tsx
@@ -11,7 +11,11 @@ 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 = Math.min(currentIndex, headings.length - 1);
+ const selectedIndex =
+ currentIndex !== undefined
+ ? Math.min(currentIndex, headings.length - 1)
+ : -1;
+
return (
{headings.length > 0 && (
@@ -51,7 +55,7 @@ export function Toc({headings}: {headings: Toc}) {
'block hover:text-link dark:hover:text-link-dark leading-normal py-2'
)}
href={h.url}>
- {h.text}
+ {h.node}
);
diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx
index cc5c654e3..f8e9023fd 100644
--- a/src/components/Layout/TopNav/TopNav.tsx
+++ b/src/components/Layout/TopNav/TopNav.tsx
@@ -14,7 +14,6 @@ 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';
@@ -27,6 +26,7 @@ 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(null);
- const {asPath} = useRouter();
+ const pathname = usePathname();
// 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);
- }, [asPath]);
+ }, [pathname]);
// 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!)
diff --git a/src/components/Layout/useTocHighlight.tsx b/src/components/Layout/useTocHighlight.tsx
index 544396c68..dd10097ac 100644
--- a/src/components/Layout/useTocHighlight.tsx
+++ b/src/components/Layout/useTocHighlight.tsx
@@ -23,7 +23,10 @@ export function getHeaderAnchors(): HTMLAnchorElement[] {
* Sets up Table of Contents highlighting.
*/
export function useTocHighlight() {
- const [currentIndex, setCurrentIndex] = useState(0);
+ const [currentIndex, setCurrentIndex] = useState(
+ undefined
+ );
+
const timeoutRef = useRef(null);
useEffect(() => {
diff --git a/src/components/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx
index 21fc6865c..ff9586ae6 100644
--- a/src/components/MDX/Challenges/Challenges.tsx
+++ b/src/components/MDX/Challenges/Challenges.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
@@ -9,7 +11,7 @@ import {H2} from 'components/MDX/Heading';
import {H4} from 'components/MDX/Heading';
import {Challenge} from './Challenge';
import {Navigation} from './Navigation';
-import {useRouter} from 'next/router';
+import {usePathname} from 'next/navigation';
interface ChallengesProps {
children: React.ReactElement[];
@@ -40,11 +42,13 @@ const parseChallengeContents = (
let challenge: Partial = {};
let content: React.ReactElement[] = [];
Children.forEach(children, (child) => {
- const {props, type} = child as React.ReactElement<{
+ const {props} = child as React.ReactElement<{
children?: string;
id?: string;
+ 'data-mdx-name'?: string;
}>;
- switch ((type as any).mdxName) {
+
+ switch (props?.['data-mdx-name']) {
case 'Solution': {
challenge.solution = child;
challenge.content = content;
@@ -90,12 +94,12 @@ export function Challenges({
const queuedScrollRef = useRef(QueuedScroll.INIT);
const [activeIndex, setActiveIndex] = useState(0);
const currentChallenge = challenges[activeIndex];
- const {asPath} = useRouter();
+ const pathname = usePathname();
useEffect(() => {
if (queuedScrollRef.current === QueuedScroll.INIT) {
const initIndex = challenges.findIndex(
- (challenge) => challenge.id === asPath.split('#')[1]
+ (challenge) => challenge.id === pathname.split('#')[1]
);
if (initIndex === -1) {
queuedScrollRef.current = undefined;
@@ -112,7 +116,7 @@ export function Challenges({
});
queuedScrollRef.current = undefined;
}
- }, [activeIndex, asPath, challenges]);
+ }, [activeIndex, pathname, challenges]);
const handleChallengeChange = (index: number) => {
setActiveIndex(index);
diff --git a/src/components/MDX/Challenges/index.tsx b/src/components/MDX/Challenges/index.tsx
index 413fd4611..d85f5eb76 100644
--- a/src/components/MDX/Challenges/index.tsx
+++ b/src/components/MDX/Challenges/index.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx
index 1fd9a8a90..1d126530f 100644
--- a/src/components/MDX/CodeBlock/CodeBlock.tsx
+++ b/src/components/MDX/CodeBlock/CodeBlock.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeBlock/index.tsx b/src/components/MDX/CodeBlock/index.tsx
index 551c1d1b6..c06fdbc81 100644
--- a/src/components/MDX/CodeBlock/index.tsx
+++ b/src/components/MDX/CodeBlock/index.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeDiagram.tsx b/src/components/MDX/CodeDiagram.tsx
index 2a198fc56..41c94efc6 100644
--- a/src/components/MDX/CodeDiagram.tsx
+++ b/src/components/MDX/CodeDiagram.tsx
@@ -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.type?.mdxName === 'pre') {
+ if (child.props?.['data-mdx-name'] === 'pre') {
return (
(shouldAutoExpand);
const [isExpanded, setIsExpanded] = useState(false);
@@ -57,8 +65,7 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
className="list-none p-8"
tabIndex={-1 /* there's a button instead */}
onClick={(e) => {
- // We toggle using a button instead of this whole area,
- // with an escape case for the header anchor link
+ // Toggle with a button instead of the whole area
if (!(e.target instanceof SVGElement)) {
e.preventDefault();
}
diff --git a/src/components/MDX/Illustration.tsx b/src/components/MDX/Illustration.tsx
new file mode 100644
index 000000000..ea674a865
--- /dev/null
+++ b/src/components/MDX/Illustration.tsx
@@ -0,0 +1,126 @@
+'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 (
+
+
+
+ Illustrated by{' '}
+ {authorLink ? (
+
+ {author}
+
+ ) : (
+ author
+ )}
+
+
+
+ );
+}
+
+export function Illustration({
+ caption,
+ src,
+ alt,
+ author,
+ authorLink,
+}: {
+ caption: string;
+ src: string;
+ alt: string;
+ author: string;
+ authorLink: string;
+}) {
+ const {isInBlock} = React.useContext(IllustrationContext);
+
+ return (
+
+
+
+ {caption ? (
+
+ {caption}
+
+ ) : null}
+
+ {!isInBlock &&
}
+
+ );
+}
+
+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) => (
+
+
+
+
+ {info.caption ? (
+
+ {info.caption}
+
+ ) : null}
+
+ ));
+ return (
+
+
+ {sequential ? (
+
+ {images.map((x: any, i: number) => (
+
+ {x}
+
+ ))}
+
+ ) : (
+
{images}
+ )}
+
+
+
+ );
+}
diff --git a/src/components/MDX/InlineCode.tsx b/src/components/MDX/InlineCode.tsx
index 5759a7c0a..a28c794c7 100644
--- a/src/components/MDX/InlineCode.tsx
+++ b/src/components/MDX/InlineCode.tsx
@@ -1,18 +1,18 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import cn from 'classnames';
-import type {HTMLAttributes} from 'react';
+import {useContext, type HTMLAttributes} from 'react';
+import {LinkContext} from './Link';
interface InlineCodeProps {
- isLink?: boolean;
meta?: string;
}
-function InlineCode({
- isLink,
- ...props
-}: HTMLAttributes & InlineCodeProps) {
+function InlineCode({...props}: HTMLAttributes & InlineCodeProps) {
+ const isLink = useContext(LinkContext);
return (
in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`
diff --git a/src/components/MDX/InlineToc.tsx b/src/components/MDX/InlineToc.tsx
new file mode 100644
index 000000000..55c52ee3d
--- /dev/null
+++ b/src/components/MDX/InlineToc.tsx
@@ -0,0 +1,60 @@
+'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;
+};
+
+type NestedTocNode = {
+ item: TocItem;
+ children: Array;
+};
+
+function calculateNestedToc(toc: Toc): NestedTocRoot {
+ const currentAncestors = new Map();
+ 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 ;
+}
+
+function InlineTocItem({items}: {items: Array}) {
+ return (
+
+ {items.map((node) => (
+
+ {node.item.node}
+ {node.children.length > 0 && }
+
+ ))}
+
+ );
+}
diff --git a/src/components/MDX/LanguageList.tsx b/src/components/MDX/LanguageList.tsx
new file mode 100644
index 000000000..2a1ea4c4b
--- /dev/null
+++ b/src/components/MDX/LanguageList.tsx
@@ -0,0 +1,39 @@
+'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 (
+
+ {languages.map(({code, name, enName}) => {
+ return (
+
+
+ {enName} ({name})
+ {' '}
+ —{' '}
+
+ Contribute
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/MDX/LanguagesContext.tsx b/src/components/MDX/LanguagesContext.tsx
index 776a11c0d..719ea4f99 100644
--- a/src/components/MDX/LanguagesContext.tsx
+++ b/src/components/MDX/LanguagesContext.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Link.tsx b/src/components/MDX/Link.tsx
index 7bf041e56..f6985fc48 100644
--- a/src/components/MDX/Link.tsx
+++ b/src/components/MDX/Link.tsx
@@ -1,13 +1,17 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
-import {Children, cloneElement} from 'react';
+import {createContext} 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,
@@ -16,36 +20,29 @@ function Link({
}: React.AnchorHTMLAttributes) {
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 ;
}
+
return (
- <>
+
{href.startsWith('https://') ? (
- {modifiedChildren}
+ {children}
) : href.startsWith('#') ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content
- {modifiedChildren}
+ {children}
) : (
- {modifiedChildren}
+ {children}
)}
- >
+
);
}
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index f24fac598..bcd4c127f 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -1,8 +1,10 @@
+// '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';
@@ -29,14 +31,13 @@ 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 (
@@ -60,27 +61,6 @@ function CodeStep({children, step}: {children: any; step: number}) {
);
}
-const P = (p: HTMLAttributes) => (
-
-);
-
-const Strong = (strong: HTMLAttributes) => (
-
-);
-
-const OL = (p: HTMLAttributes) => (
-
-);
-const LI = (p: HTMLAttributes) => (
-
-);
-const UL = (p: HTMLAttributes) => (
-
-);
-
-const Divider = () => (
-
-);
const Wip = ({children}: {children: React.ReactNode}) => (
{children}
);
@@ -232,214 +212,6 @@ function Recipes(props: any) {
return ;
}
-function AuthorCredit({
- author = 'Rachel Lee Nabors',
- authorLink = 'https://nearestnabors.com/',
-}: {
- author: string;
- authorLink: string;
-}) {
- return (
-
-
-
- Illustrated by{' '}
- {authorLink ? (
-
- {author}
-
- ) : (
- author
- )}
-
-
-
- );
-}
-
-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 (
-
-
-
- {caption ? (
-
- {caption}
-
- ) : null}
-
- {!isInBlock &&
}
-
- );
-}
-
-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) => (
-
-
-
-
- {info.caption ? (
-
- {info.caption}
-
- ) : null}
-
- ));
- return (
-
-
- {sequential ? (
-
- {images.map((x: any, i: number) => (
-
- {x}
-
- ))}
-
- ) : (
-
{images}
- )}
-
-
-
- );
-}
-
-type NestedTocRoot = {
- item: null;
- children: Array;
-};
-
-type NestedTocNode = {
- item: TocItem;
- children: Array;
-};
-
-function calculateNestedToc(toc: Toc): NestedTocRoot {
- const currentAncestors = new Map();
- 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 ;
-}
-
-function InlineTocItem({items}: {items: Array}) {
- return (
-
- {items.map((node) => (
-
- {node.item.text}
- {node.children.length > 0 && }
-
- ))}
-
- );
-}
-
-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 (
-
- {languages.map(({code, name, enName}) => {
- return (
-
-
- {enName} ({name})
- {' '}
- —{' '}
-
- Contribute
-
-
- );
- })}
-
- );
-}
-
function YouTubeIframe(props: any) {
return (
@@ -460,7 +232,22 @@ function Image(props: any) {
return
;
}
-export const MDXComponents = {
+function annotateMDXComponents(
+ components: Record
+): Record {
+ return Object.entries(components).reduce((acc, [key, Component]) => {
+ acc[key] = (props) => ;
+ acc[key].displayName = `Annotated(${key})`; // Optional, for debugging
+ return acc;
+ }, {} as Record);
+}
+
+export const MDXComponentsToc = annotateMDXComponents({
+ a: Link,
+ code: InlineCode,
+});
+
+export const MDXComponents = annotateMDXComponents({
p: P,
strong: Strong,
blockquote: Blockquote,
@@ -529,11 +316,4 @@ export const MDXComponents = {
CodeStep,
YouTubeIframe,
ErrorDecoder,
-};
-
-for (let key in MDXComponents) {
- if (MDXComponents.hasOwnProperty(key)) {
- const MDXComponent: any = (MDXComponents as any)[key];
- MDXComponent.mdxName = key;
- }
-}
+});
diff --git a/src/components/MDX/PackageImport.tsx b/src/components/MDX/PackageImport.tsx
index 5e2da820e..a4d5fa140 100644
--- a/src/components/MDX/PackageImport.tsx
+++ b/src/components/MDX/PackageImport.tsx
@@ -12,10 +12,10 @@ interface PackageImportProps {
export function PackageImport({children}: PackageImportProps) {
const terminal = Children.toArray(children).filter((child: any) => {
- return child.type?.mdxName !== 'pre';
+ return child.props?.['data-mdx-name'] !== 'pre';
});
const code = Children.toArray(children).map((child: any, i: number) => {
- if (child.type?.mdxName === 'pre') {
+ if (child.props?.['data-mdx-name'] === 'pre') {
return (
) => (
+
+);
+
+export const Strong = (strong: HTMLAttributes) => (
+
+);
+
+export const OL = (p: HTMLAttributes) => (
+
+);
+export const LI = (p: HTMLAttributes) => (
+
+);
+export const UL = (p: HTMLAttributes) => (
+
+);
+
+export const Divider = () => (
+
+);
diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx
index 7d6e566d2..f95d3270a 100644
--- a/src/components/MDX/Sandpack/CustomPreset.tsx
+++ b/src/components/MDX/Sandpack/CustomPreset.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Sandpack/SandpackRoot.tsx b/src/components/MDX/Sandpack/SandpackRoot.tsx
index 67f40d0b3..1084ea647 100644
--- a/src/components/MDX/Sandpack/SandpackRoot.tsx
+++ b/src/components/MDX/Sandpack/SandpackRoot.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts
index 193b07be8..07bdcd377 100644
--- a/src/components/MDX/Sandpack/createFileMap.ts
+++ b/src/components/MDX/Sandpack/createFileMap.ts
@@ -12,19 +12,22 @@ export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];
export const createFileMap = (codeSnippets: any) => {
return codeSnippets.reduce(
(result: Record, codeSnippet: React.ReactElement) => {
- if (
- (codeSnippet.type as any).mdxName !== 'pre' &&
- codeSnippet.type !== 'pre'
- ) {
- return result;
- }
+ // TODO: actually fix this
const {props} = (
codeSnippet.props as PropsWithChildren<{
children: ReactElement<
- HTMLAttributes & {meta?: string}
+ HTMLAttributes & {
+ meta?: string;
+ 'data-mdx-name'?: 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
diff --git a/src/components/MDX/Sandpack/index.tsx b/src/components/MDX/Sandpack/index.tsx
index 6755ba8de..d90facfe8 100644
--- a/src/components/MDX/Sandpack/index.tsx
+++ b/src/components/MDX/Sandpack/index.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/SandpackWithHTMLOutput.tsx b/src/components/MDX/SandpackWithHTMLOutput.tsx
index 51ce28dc1..041d7bf9b 100644
--- a/src/components/MDX/SandpackWithHTMLOutput.tsx
+++ b/src/components/MDX/SandpackWithHTMLOutput.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import {Children, memo} from 'react';
import InlineCode from './InlineCode';
import Sandpack from './Sandpack';
diff --git a/src/components/MDX/TerminalBlock.tsx b/src/components/MDX/TerminalBlock.tsx
index 475292716..73a102167 100644
--- a/src/components/MDX/TerminalBlock.tsx
+++ b/src/components/MDX/TerminalBlock.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/TocContext.tsx b/src/components/MDX/TocContext.tsx
index 8aeead370..cc7080a8b 100644
--- a/src/components/MDX/TocContext.tsx
+++ b/src/components/MDX/TocContext.tsx
@@ -7,7 +7,7 @@ import type {ReactNode} from 'react';
export type TocItem = {
url: string;
- text: ReactNode;
+ node: ReactNode;
depth: number;
};
export type Toc = Array;
diff --git a/src/components/SafariScrollHandler.tsx b/src/components/SafariScrollHandler.tsx
new file mode 100644
index 000000000..2cb3e4037
--- /dev/null
+++ b/src/components/SafariScrollHandler.tsx
@@ -0,0 +1,22 @@
+'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;
+}
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index c7401487b..3ff5c1881 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -4,7 +4,7 @@
import Head from 'next/head';
import Link from 'next/link';
-import Router from 'next/router';
+import {useRouter} from 'next/navigation';
import {lazy, useEffect} from 'react';
import * as React from 'react';
import {createPortal} from 'react-dom';
@@ -111,6 +111,7 @@ export function Search({
},
}: SearchProps) {
useDocSearchKeyboardEvents({isOpen, onOpen, onClose});
+ const router = useRouter();
return (
<>
@@ -127,7 +128,7 @@ export function Search({
onClose={onClose}
navigator={{
navigate({itemUrl}: any) {
- Router.push(itemUrl);
+ router.push(itemUrl);
},
}}
transformItems={(items: any[]) => {
diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx
deleted file mode 100644
index 628085744..000000000
--- a/src/components/Seo.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * 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;
- 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 (
-
-
- {title != null && {pageTitle} }
- {isHomePage && (
- // Let Google figure out a good description for each page.
-
- )}
-
-
- {finishedTranslations.map((languageCode) => (
-
- ))}
-
-
-
- {title != null && (
-
- )}
- {description != null && (
-
- )}
-
-
-
-
- {title != null && (
-
- )}
- {description != null && (
-
- )}
-
-
- {searchOrder != null && (
-
- )}
-
-
-
-
-
-
-
-
- {children}
-
- );
- }
-);
diff --git a/src/components/ThemeScript.jsx b/src/components/ThemeScript.jsx
new file mode 100644
index 000000000..66034557c
--- /dev/null
+++ b/src/components/ThemeScript.jsx
@@ -0,0 +1,52 @@
+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 (
+
+ );
+}
diff --git a/src/components/UwuScript.jsx b/src/components/UwuScript.jsx
new file mode 100644
index 000000000..d2c26585d
--- /dev/null
+++ b/src/components/UwuScript.jsx
@@ -0,0 +1,64 @@
+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 (
+
+ );
+}
diff --git a/src/content/community/docs-contributors.md b/src/content/community/docs-contributors.md
index 27b32a18f..4f4567846 100644
--- a/src/content/community/docs-contributors.md
+++ b/src/content/community/docs-contributors.md
@@ -38,5 +38,6 @@ 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.
diff --git a/src/content/versions.md b/src/content/versions.md
index 8530f6324..8956e80e8 100644
--- a/src/content/versions.md
+++ b/src/content/versions.md
@@ -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:
-VIDEO
+VIDEO
diff --git a/src/hooks/usePendingRoute.ts b/src/hooks/usePendingRoute.ts
index 229a36e64..2e3f7c6f6 100644
--- a/src/hooks/usePendingRoute.ts
+++ b/src/hooks/usePendingRoute.ts
@@ -2,40 +2,9 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
-import {useRouter} from 'next/router';
-import {useState, useRef, useEffect} from 'react';
-
const usePendingRoute = () => {
- const {events} = useRouter();
- const [pendingRoute, setPendingRoute] = useState(null);
- const currentRoute = useRef(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;
+ // TODO: @feedthejim - Implement usePendingRoute when App Router supports tapping into the transition state
+ return null;
};
export default usePendingRoute;
diff --git a/src/instrumentation.js b/src/instrumentation.js
new file mode 100644
index 000000000..94c964cea
--- /dev/null
+++ b/src/instrumentation.js
@@ -0,0 +1,34 @@
+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();
+ });
+ }
+}
diff --git a/src/pages/[[...markdownPath]].js b/src/pages/[[...markdownPath]].js
deleted file mode 100644
index bef4508df..000000000
--- a/src/pages/[[...markdownPath]].js
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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 (
-
- {parsedContent}
-
- );
-}
-
-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 ;
- } 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,
- };
-}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
deleted file mode 100644
index 5431f87cc..000000000
--- a/src/pages/_app.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 ;
-}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
deleted file mode 100644
index 6849df35d..000000000
--- a/src/pages/_document.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * Copyright (c) Facebook, Inc. and its affiliates.
- */
-
-import {Html, Head, Main, NextScript} from 'next/document';
-import {siteConfig} from '../siteConfig';
-
-const MyDocument = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default MyDocument;
diff --git a/src/pages/errors/[errorCode].tsx b/src/pages/errors/[errorCode].tsx
deleted file mode 100644
index de9eab5bb..000000000
--- a/src/pages/errors/[errorCode].tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-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) {
- const parsedContent = useMemo(
- () => JSON.parse(content, reviveNodeOnClient),
- [content]
- );
-
- return (
-
-
- {parsedContent}
- {/*
-
- 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.
-
-
- */}
-
-
- );
-}
-
-// 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 ;
- } 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 | null = null;
-
-export const getStaticProps: GetStaticProps = 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',
- };
-};
diff --git a/src/pages/errors/index.tsx b/src/pages/errors/index.tsx
deleted file mode 100644
index d7742f03a..000000000
--- a/src/pages/errors/index.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import ErrorDecoderPage from './[errorCode]';
-export default ErrorDecoderPage;
-export {getStaticProps} from './[errorCode]';
diff --git a/src/styles/index.css b/src/styles/index.css
index 281111092..28d8473ac 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -19,8 +19,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -28,8 +27,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_MdIt.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_MdIt.woff2') format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap;
@@ -37,8 +35,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_SBd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_SBd.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -46,8 +43,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_SBdIt.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_SBdIt.woff2') format('woff2');
font-weight: 600;
font-style: italic;
font-display: swap;
@@ -55,8 +51,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -64,8 +59,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_W_BdIt.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_W_BdIt.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
@@ -73,8 +67,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_Rg.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_Rg.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -82,8 +75,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_It.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_It.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
@@ -91,8 +83,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -100,8 +91,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_MdIt.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_MdIt.woff2') format('woff2');
font-weight: 500;
font-style: italic;
font-display: swap;
@@ -109,8 +99,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -118,8 +107,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_W_BdIt.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_W_BdIt.woff2') format('woff2');
font-weight: 700;
font-style: italic;
font-display: swap;
@@ -129,8 +117,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Arbc_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -139,8 +126,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_SBd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Arbc_W_SBd.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -149,8 +135,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Arbc_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -159,8 +144,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Rg.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Arbc_W_Rg.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -169,8 +153,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Arbc_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -179,8 +162,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Arbc_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -191,8 +173,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Cyrl_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -201,8 +182,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_SBd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Cyrl_W_SBd.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -211,8 +191,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Cyrl_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -221,8 +200,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Rg.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Cyrl_W_Rg.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -231,8 +209,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Cyrl_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -241,8 +218,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Cyrl_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -253,8 +229,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Deva_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -264,8 +239,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_SBd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Deva_W_SBd.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -275,8 +249,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Deva_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -286,8 +259,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Rg.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Deva_W_Rg.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -297,8 +269,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Deva_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -308,8 +279,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Deva_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -321,8 +291,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Viet_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -331,8 +300,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_SBd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Viet_W_SBd.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -341,8 +309,7 @@
@font-face {
font-family: 'Optimistic Display';
- src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Display_Viet_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
@@ -351,8 +318,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Rg.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Viet_W_Rg.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -361,8 +327,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Md.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Viet_W_Md.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
@@ -371,8 +336,7 @@
@font-face {
font-family: 'Optimistic Text';
- src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Bd.woff2')
- format('woff2');
+ src: url('/fonts/Optimistic_Text_Viet_W_Bd.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
diff --git a/src/utils/compileMDX.ts b/src/utils/compileMDX.ts
deleted file mode 100644
index be770c29a..000000000
--- a/src/utils/compileMDX.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-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 | 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;
-}
diff --git a/src/utils/generateMDX.tsx b/src/utils/generateMDX.tsx
new file mode 100644
index 000000000..4d24e5f7a
--- /dev/null
+++ b/src/utils/generateMDX.tsx
@@ -0,0 +1,148 @@
+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 {
+ 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 {
+ 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 {
+ 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: ,
+ 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: ,
+ toc: tocWithMDX,
+ meta: result.meta,
+ };
+}
+
+async function getTransformedToc(toc: ExtractedTOC[]): Promise {
+ 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 = ;
+
+ return item;
+ })
+ );
+}
diff --git a/src/utils/generateMetadata.ts b/src/utils/generateMetadata.ts
new file mode 100644
index 000000000..e45b10dab
--- /dev/null
+++ b/src/utils/generateMetadata.ts
@@ -0,0 +1,75 @@
+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(),
+ }),
+ },
+ };
+}
diff --git a/src/utils/mdx/MaxWidthWrapperPlugin.ts b/src/utils/mdx/MaxWidthWrapperPlugin.ts
new file mode 100644
index 000000000..22f1a022f
--- /dev/null
+++ b/src/utils/mdx/MaxWidthWrapperPlugin.ts
@@ -0,0 +1,48 @@
+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;
+ };
+}
diff --git a/src/utils/mdx/MetaAttributesPlugin.ts b/src/utils/mdx/MetaAttributesPlugin.ts
new file mode 100644
index 000000000..f708fd368
--- /dev/null
+++ b/src/utils/mdx/MetaAttributesPlugin.ts
@@ -0,0 +1,19 @@
+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;
+ }
+ });
+ };
+}
diff --git a/src/utils/mdx/TOCExtractorPlugin.ts b/src/utils/mdx/TOCExtractorPlugin.ts
new file mode 100644
index 000000000..dd8e4d644
--- /dev/null
+++ b/src/utils/mdx/TOCExtractorPlugin.ts
@@ -0,0 +1,131 @@
+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., )
+ 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;
+ };
+}
diff --git a/src/utils/prepareMDX.js b/src/utils/prepareMDX.js
deleted file mode 100644
index 20a22577d..000000000
--- a/src/utils/prepareMDX.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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
- // 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({wrapQueue} );
- 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);
- }
- }
-}
diff --git a/src/utils/rss.js b/src/utils/rss.js
deleted file mode 100644
index 29e5511ea..000000000
--- a/src/utils/rss.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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}));
-};
diff --git a/tailwind.config.js b/tailwind.config.js
index f31a24516..f709aba7d 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -9,6 +9,7 @@ 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',
diff --git a/tsconfig.json b/tsconfig.json
index 440b38510..983db114c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,16 +1,12 @@
{
"compilerOptions": {
"target": "es5",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
- "noImplicitReturns": true,
+ "noImplicitReturns": false,
"noImplicitThis": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
@@ -22,14 +18,20 @@
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "src",
- "incremental": true
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
},
"include": [
"next-env.d.ts",
"src/**/*.ts",
- "src/**/*.tsx"
+ "src/**/*.tsx",
+ ".next/types/**/*.ts",
+ "types.d.ts",
+ "src/components/UwuScript.jsx"
],
- "exclude": [
- "node_modules"
- ]
+ "exclude": ["node_modules"]
}
diff --git a/types.d.ts b/types.d.ts
new file mode 100644
index 000000000..72e9cbfa1
--- /dev/null
+++ b/types.d.ts
@@ -0,0 +1,8 @@
+declare module 'metro-cache' {
+ export class FileStore {
+ constructor(options: {root: string});
+ get(hash: Buffer): Promise;
+ set(hash: Buffer, value: Buffer): Promise;
+ }
+ export function stableHash(obj: any): string;
+}
diff --git a/yarn.lock b/yarn.lock
index a118cbeda..2e33a440c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1225,10 +1225,10 @@
unist-util-visit "^4.0.0"
vfile "^5.0.0"
-"@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/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/eslint-plugin-next@12.0.3":
version "12.0.3"
@@ -1237,45 +1237,45 @@
dependencies:
glob "7.1.7"
-"@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.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.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.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.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.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.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.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==
+"@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-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-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-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-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-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-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-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==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1656,6 +1656,13 @@
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"
@@ -1724,6 +1731,11 @@
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"
@@ -2423,15 +2435,10 @@ 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:
- 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==
+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==
ccount@^1.0.0:
version "1.1.0"
@@ -2515,6 +2522,13 @@ 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"
@@ -5798,12 +5812,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.1.0:
- version "15.1.0"
- resolved "https://registry.yarnpkg.com/next/-/next-15.1.0.tgz#be847cf67ac94ae23b57f3ea6d10642f3fc1ad69"
- integrity sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==
+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==
dependencies:
- "@next/env" "15.1.0"
+ "@next/env" "15.2.0-canary.33"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
@@ -5811,14 +5825,14 @@ next@15.1.0:
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
- "@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"
+ "@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"
sharp "^0.33.5"
nice-try@^1.0.4:
@@ -6800,6 +6814,11 @@ 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"
@@ -8087,6 +8106,13 @@ 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"
From af0358f1d2eebd6f15916b04ef7e8b910247a94b Mon Sep 17 00:00:00 2001
From: dan
Date: Sat, 1 Feb 2025 19:21:43 +0000
Subject: [PATCH 108/109] Revert "feat: migrate React.dev to the App Router
(#7437)" (#7466)
This reverts commit 0eb0f889b1ad19eca5cf0e303945855ffe6f05b4.
---
.eslintrc | 4 +-
next-env.d.ts | 2 +-
next.config.js | 3 -
package.json | 14 +-
scripts/generateRss.js | 6 +
src/app/[[...markdownPath]]/page.tsx | 172 -----------
src/app/errors/[[...errorCode]]/page.tsx | 118 --------
src/app/layout.tsx | 157 ----------
src/app/rss.xml/route.js | 69 -----
src/components/Analytics.tsx | 63 ----
src/components/DevContentRefresher.tsx | 29 --
src/components/ErrorDecoderProvider.tsx | 19 --
src/components/Layout/Feedback.tsx | 6 +-
src/components/Layout/Page.tsx | 51 ++--
.../Layout/Sidebar/SidebarRouteTree.tsx | 4 +-
src/components/Layout/Toc.tsx | 8 +-
src/components/Layout/TopNav/TopNav.tsx | 6 +-
src/components/Layout/useTocHighlight.tsx | 5 +-
src/components/MDX/Challenges/Challenges.tsx | 16 +-
src/components/MDX/Challenges/index.tsx | 2 -
src/components/MDX/CodeBlock/CodeBlock.tsx | 2 -
src/components/MDX/CodeBlock/index.tsx | 2 -
src/components/MDX/CodeDiagram.tsx | 2 +-
src/components/MDX/ErrorDecoder.tsx | 2 -
src/components/MDX/ExpandableExample.tsx | 19 +-
src/components/MDX/Illustration.tsx | 126 --------
src/components/MDX/InlineCode.tsx | 12 +-
src/components/MDX/InlineToc.tsx | 60 ----
src/components/MDX/LanguageList.tsx | 39 ---
src/components/MDX/LanguagesContext.tsx | 2 -
src/components/MDX/Link.tsx | 25 +-
src/components/MDX/MDXComponents.tsx | 268 ++++++++++++++++--
src/components/MDX/PackageImport.tsx | 4 +-
src/components/MDX/Primitives.tsx | 23 --
src/components/MDX/Sandpack/CustomPreset.tsx | 2 -
src/components/MDX/Sandpack/SandpackRoot.tsx | 2 -
src/components/MDX/Sandpack/createFileMap.ts | 17 +-
src/components/MDX/Sandpack/index.tsx | 2 -
src/components/MDX/SandpackWithHTMLOutput.tsx | 2 -
src/components/MDX/TerminalBlock.tsx | 2 -
src/components/MDX/TocContext.tsx | 2 +-
src/components/SafariScrollHandler.tsx | 22 --
src/components/Search.tsx | 5 +-
src/components/Seo.tsx | 185 ++++++++++++
src/components/ThemeScript.jsx | 52 ----
src/components/UwuScript.jsx | 64 -----
src/content/community/docs-contributors.md | 1 -
src/content/versions.md | 2 +-
src/hooks/usePendingRoute.ts | 35 ++-
src/instrumentation.js | 34 ---
src/{app/not-found.tsx => pages/404.js} | 17 +-
src/{app/error.tsx => pages/500.js} | 18 +-
src/pages/[[...markdownPath]].js | 179 ++++++++++++
src/pages/_app.tsx | 58 ++++
src/pages/_document.tsx | 158 +++++++++++
src/pages/errors/[errorCode].tsx | 153 ++++++++++
src/pages/errors/index.tsx | 3 +
src/styles/index.css | 108 ++++---
src/utils/compileMDX.ts | 168 +++++++++++
src/utils/generateMDX.tsx | 148 ----------
src/utils/generateMetadata.ts | 75 -----
src/utils/mdx/MaxWidthWrapperPlugin.ts | 48 ----
src/utils/mdx/MetaAttributesPlugin.ts | 19 --
src/utils/mdx/TOCExtractorPlugin.ts | 131 ---------
src/utils/prepareMDX.js | 117 ++++++++
src/utils/rss.js | 82 ++++++
tailwind.config.js | 1 -
tsconfig.json | 24 +-
types.d.ts | 8 -
yarn.lock | 156 +++++-----
70 files changed, 1635 insertions(+), 1805 deletions(-)
create mode 100644 scripts/generateRss.js
delete mode 100644 src/app/[[...markdownPath]]/page.tsx
delete mode 100644 src/app/errors/[[...errorCode]]/page.tsx
delete mode 100644 src/app/layout.tsx
delete mode 100644 src/app/rss.xml/route.js
delete mode 100644 src/components/Analytics.tsx
delete mode 100644 src/components/DevContentRefresher.tsx
delete mode 100644 src/components/ErrorDecoderProvider.tsx
delete mode 100644 src/components/MDX/Illustration.tsx
delete mode 100644 src/components/MDX/InlineToc.tsx
delete mode 100644 src/components/MDX/LanguageList.tsx
delete mode 100644 src/components/MDX/Primitives.tsx
delete mode 100644 src/components/SafariScrollHandler.tsx
create mode 100644 src/components/Seo.tsx
delete mode 100644 src/components/ThemeScript.jsx
delete mode 100644 src/components/UwuScript.jsx
delete mode 100644 src/instrumentation.js
rename src/{app/not-found.tsx => pages/404.js} (60%)
rename src/{app/error.tsx => pages/500.js} (61%)
create mode 100644 src/pages/[[...markdownPath]].js
create mode 100644 src/pages/_app.tsx
create mode 100644 src/pages/_document.tsx
create mode 100644 src/pages/errors/[errorCode].tsx
create mode 100644 src/pages/errors/index.tsx
create mode 100644 src/utils/compileMDX.ts
delete mode 100644 src/utils/generateMDX.tsx
delete mode 100644 src/utils/generateMetadata.ts
delete mode 100644 src/utils/mdx/MaxWidthWrapperPlugin.ts
delete mode 100644 src/utils/mdx/MetaAttributesPlugin.ts
delete mode 100644 src/utils/mdx/TOCExtractorPlugin.ts
create mode 100644 src/utils/prepareMDX.js
create mode 100644 src/utils/rss.js
delete mode 100644 types.d.ts
diff --git a/.eslintrc b/.eslintrc
index 2af52fb53..f8b03f98a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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,
diff --git a/next-env.d.ts b/next-env.d.ts
index 1b3be0840..52e831b43 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -2,4 +2,4 @@
///
// 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.
diff --git a/next.config.js b/next.config.js
index bf45dd5b3..861792c8e 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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');
diff --git a/package.json b/package.json
index 3fd49e67c..6d6b53f92 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/scripts/generateRss.js b/scripts/generateRss.js
new file mode 100644
index 000000000..e0f3d5561
--- /dev/null
+++ b/scripts/generateRss.js
@@ -0,0 +1,6 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ */
+const {generateRssFeed} = require('../src/utils/rss');
+
+generateRssFeed();
diff --git a/src/app/[[...markdownPath]]/page.tsx b/src/app/[[...markdownPath]]/page.tsx
deleted file mode 100644
index 85dc60700..000000000
--- a/src/app/[[...markdownPath]]/page.tsx
+++ /dev/null
@@ -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 {
- 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 | 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 (
-
- {content}
-
- );
-}
-// 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,
- });
-}
diff --git a/src/app/errors/[[...errorCode]]/page.tsx b/src/app/errors/[[...errorCode]]/page.tsx
deleted file mode 100644
index b9158d0a5..000000000
--- a/src/app/errors/[[...errorCode]]/page.tsx
+++ /dev/null
@@ -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 | 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 (
-
-
- {content}
-
-
- );
-}
-
-// 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,
- });
-}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
deleted file mode 100644
index 079a0a874..000000000
--- a/src/app/layout.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-export default function RootLayout({children}: {children: React.ReactNode}) {
- return (
-
-
-
-
-
-
-
-
- {process.env.NODE_ENV !== 'production' && }
- {children}
-
-
-
-
-
- );
-}
diff --git a/src/app/rss.xml/route.js b/src/app/rss.xml/route.js
deleted file mode 100644
index 9ade331e7..000000000
--- a/src/app/rss.xml/route.js
+++ /dev/null
@@ -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';
diff --git a/src/components/Analytics.tsx b/src/components/Analytics.tsx
deleted file mode 100644
index 07d637e86..000000000
--- a/src/components/Analytics.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- >
- );
-}
diff --git a/src/components/DevContentRefresher.tsx b/src/components/DevContentRefresher.tsx
deleted file mode 100644
index 31a0961ed..000000000
--- a/src/components/DevContentRefresher.tsx
+++ /dev/null
@@ -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(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;
-}
diff --git a/src/components/ErrorDecoderProvider.tsx b/src/components/ErrorDecoderProvider.tsx
deleted file mode 100644
index bad1ed2d0..000000000
--- a/src/components/ErrorDecoderProvider.tsx
+++ /dev/null
@@ -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 (
-
- {children}
-
- );
-}
diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx
index 16b974c10..34db728ce 100644
--- a/src/components/Layout/Feedback.tsx
+++ b/src/components/Layout/Feedback.tsx
@@ -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 ;
}
diff --git a/src/components/Layout/Page.tsx b/src/components/Layout/Page.tsx
index ea0c53e1c..24d379589 100644
--- a/src/components/Layout/Page.tsx
+++ b/src/components/Layout/Page.tsx
@@ -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 (
<>
+
{(isHomePage || isBlogIndex) && (
- // RSS Feed link is now handled by metadata in layout.tsx
-
+
+
+
)}
+ {/* */}
-
+
{content}
- {showToc && toc.length > 0 && }
+ {showToc && toc.length > 0 && }
>
diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx
index f67b0ed2b..72003df74 100644
--- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx
+++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx
@@ -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 (
diff --git a/src/components/Layout/Toc.tsx b/src/components/Layout/Toc.tsx
index a8d269898..5308c602c 100644
--- a/src/components/Layout/Toc.tsx
+++ b/src/components/Layout/Toc.tsx
@@ -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 (
{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}
);
diff --git a/src/components/Layout/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx
index f8e9023fd..cc5c654e3 100644
--- a/src/components/Layout/TopNav/TopNav.tsx
+++ b/src/components/Layout/TopNav/TopNav.tsx
@@ -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(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!)
diff --git a/src/components/Layout/useTocHighlight.tsx b/src/components/Layout/useTocHighlight.tsx
index dd10097ac..544396c68 100644
--- a/src/components/Layout/useTocHighlight.tsx
+++ b/src/components/Layout/useTocHighlight.tsx
@@ -23,10 +23,7 @@ export function getHeaderAnchors(): HTMLAnchorElement[] {
* Sets up Table of Contents highlighting.
*/
export function useTocHighlight() {
- const [currentIndex, setCurrentIndex] = useState(
- undefined
- );
-
+ const [currentIndex, setCurrentIndex] = useState(0);
const timeoutRef = useRef(null);
useEffect(() => {
diff --git a/src/components/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx
index ff9586ae6..21fc6865c 100644
--- a/src/components/MDX/Challenges/Challenges.tsx
+++ b/src/components/MDX/Challenges/Challenges.tsx
@@ -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 = {};
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(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);
diff --git a/src/components/MDX/Challenges/index.tsx b/src/components/MDX/Challenges/index.tsx
index d85f5eb76..413fd4611 100644
--- a/src/components/MDX/Challenges/index.tsx
+++ b/src/components/MDX/Challenges/index.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx
index 1d126530f..1fd9a8a90 100644
--- a/src/components/MDX/CodeBlock/CodeBlock.tsx
+++ b/src/components/MDX/CodeBlock/CodeBlock.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeBlock/index.tsx b/src/components/MDX/CodeBlock/index.tsx
index c06fdbc81..551c1d1b6 100644
--- a/src/components/MDX/CodeBlock/index.tsx
+++ b/src/components/MDX/CodeBlock/index.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/CodeDiagram.tsx b/src/components/MDX/CodeDiagram.tsx
index 41c94efc6..2a198fc56 100644
--- a/src/components/MDX/CodeDiagram.tsx
+++ b/src/components/MDX/CodeDiagram.tsx
@@ -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 (
(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();
}
diff --git a/src/components/MDX/Illustration.tsx b/src/components/MDX/Illustration.tsx
deleted file mode 100644
index ea674a865..000000000
--- a/src/components/MDX/Illustration.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Illustrated by{' '}
- {authorLink ? (
-
- {author}
-
- ) : (
- author
- )}
-
-
-
- );
-}
-
-export function Illustration({
- caption,
- src,
- alt,
- author,
- authorLink,
-}: {
- caption: string;
- src: string;
- alt: string;
- author: string;
- authorLink: string;
-}) {
- const {isInBlock} = React.useContext(IllustrationContext);
-
- return (
-
-
-
- {caption ? (
-
- {caption}
-
- ) : null}
-
- {!isInBlock &&
}
-
- );
-}
-
-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) => (
-
-
-
-
- {info.caption ? (
-
- {info.caption}
-
- ) : null}
-
- ));
- return (
-
-
- {sequential ? (
-
- {images.map((x: any, i: number) => (
-
- {x}
-
- ))}
-
- ) : (
-
{images}
- )}
-
-
-
- );
-}
diff --git a/src/components/MDX/InlineCode.tsx b/src/components/MDX/InlineCode.tsx
index a28c794c7..5759a7c0a 100644
--- a/src/components/MDX/InlineCode.tsx
+++ b/src/components/MDX/InlineCode.tsx
@@ -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 & InlineCodeProps) {
- const isLink = useContext(LinkContext);
+function InlineCode({
+ isLink,
+ ...props
+}: HTMLAttributes & InlineCodeProps) {
return (
in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`
diff --git a/src/components/MDX/InlineToc.tsx b/src/components/MDX/InlineToc.tsx
deleted file mode 100644
index 55c52ee3d..000000000
--- a/src/components/MDX/InlineToc.tsx
+++ /dev/null
@@ -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;
-};
-
-type NestedTocNode = {
- item: TocItem;
- children: Array;
-};
-
-function calculateNestedToc(toc: Toc): NestedTocRoot {
- const currentAncestors = new Map();
- 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 ;
-}
-
-function InlineTocItem({items}: {items: Array}) {
- return (
-
- {items.map((node) => (
-
- {node.item.node}
- {node.children.length > 0 && }
-
- ))}
-
- );
-}
diff --git a/src/components/MDX/LanguageList.tsx b/src/components/MDX/LanguageList.tsx
deleted file mode 100644
index 2a1ea4c4b..000000000
--- a/src/components/MDX/LanguageList.tsx
+++ /dev/null
@@ -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 (
-
- {languages.map(({code, name, enName}) => {
- return (
-
-
- {enName} ({name})
- {' '}
- —{' '}
-
- Contribute
-
-
- );
- })}
-
- );
-}
diff --git a/src/components/MDX/LanguagesContext.tsx b/src/components/MDX/LanguagesContext.tsx
index 719ea4f99..776a11c0d 100644
--- a/src/components/MDX/LanguagesContext.tsx
+++ b/src/components/MDX/LanguagesContext.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Link.tsx b/src/components/MDX/Link.tsx
index f6985fc48..7bf041e56 100644
--- a/src/components/MDX/Link.tsx
+++ b/src/components/MDX/Link.tsx
@@ -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) {
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 ;
}
-
return (
-
+ <>
{href.startsWith('https://') ? (
- {children}
+ {modifiedChildren}
) : href.startsWith('#') ? (
// eslint-disable-next-line jsx-a11y/anchor-has-content
- {children}
+ {modifiedChildren}
) : (
- {children}
+ {modifiedChildren}
)}
-
+ >
);
}
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index bcd4c127f..f24fac598 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -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) => (
+
+);
+
+const Strong = (strong: HTMLAttributes) => (
+
+);
+
+const OL = (p: HTMLAttributes) => (
+
+);
+const LI = (p: HTMLAttributes) => (
+
+);
+const UL = (p: HTMLAttributes) => (
+
+);
+
+const Divider = () => (
+
+);
const Wip = ({children}: {children: React.ReactNode}) => (
{children}
);
@@ -212,6 +232,214 @@ function Recipes(props: any) {
return ;
}
+function AuthorCredit({
+ author = 'Rachel Lee Nabors',
+ authorLink = 'https://nearestnabors.com/',
+}: {
+ author: string;
+ authorLink: string;
+}) {
+ return (
+
+
+
+ Illustrated by{' '}
+ {authorLink ? (
+
+ {author}
+
+ ) : (
+ author
+ )}
+
+
+
+ );
+}
+
+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 (
+
+
+
+ {caption ? (
+
+ {caption}
+
+ ) : null}
+
+ {!isInBlock &&
}
+
+ );
+}
+
+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) => (
+
+
+
+
+ {info.caption ? (
+
+ {info.caption}
+
+ ) : null}
+
+ ));
+ return (
+
+
+ {sequential ? (
+
+ {images.map((x: any, i: number) => (
+
+ {x}
+
+ ))}
+
+ ) : (
+
{images}
+ )}
+
+
+
+ );
+}
+
+type NestedTocRoot = {
+ item: null;
+ children: Array;
+};
+
+type NestedTocNode = {
+ item: TocItem;
+ children: Array;
+};
+
+function calculateNestedToc(toc: Toc): NestedTocRoot {
+ const currentAncestors = new Map();
+ 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 ;
+}
+
+function InlineTocItem({items}: {items: Array}) {
+ return (
+
+ {items.map((node) => (
+
+ {node.item.text}
+ {node.children.length > 0 && }
+
+ ))}
+
+ );
+}
+
+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 (
+
+ {languages.map(({code, name, enName}) => {
+ return (
+
+
+ {enName} ({name})
+ {' '}
+ —{' '}
+
+ Contribute
+
+
+ );
+ })}
+
+ );
+}
+
function YouTubeIframe(props: any) {
return (
@@ -232,22 +460,7 @@ function Image(props: any) {
return
;
}
-function annotateMDXComponents(
- components: Record
-): Record {
- return Object.entries(components).reduce((acc, [key, Component]) => {
- acc[key] = (props) => ;
- acc[key].displayName = `Annotated(${key})`; // Optional, for debugging
- return acc;
- }, {} as Record);
-}
-
-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;
+ }
+}
diff --git a/src/components/MDX/PackageImport.tsx b/src/components/MDX/PackageImport.tsx
index a4d5fa140..5e2da820e 100644
--- a/src/components/MDX/PackageImport.tsx
+++ b/src/components/MDX/PackageImport.tsx
@@ -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 (
) => (
-
-);
-
-export const Strong = (strong: HTMLAttributes) => (
-
-);
-
-export const OL = (p: HTMLAttributes) => (
-
-);
-export const LI = (p: HTMLAttributes) => (
-
-);
-export const UL = (p: HTMLAttributes) => (
-
-);
-
-export const Divider = () => (
-
-);
diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx
index f95d3270a..7d6e566d2 100644
--- a/src/components/MDX/Sandpack/CustomPreset.tsx
+++ b/src/components/MDX/Sandpack/CustomPreset.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Sandpack/SandpackRoot.tsx b/src/components/MDX/Sandpack/SandpackRoot.tsx
index 1084ea647..67f40d0b3 100644
--- a/src/components/MDX/Sandpack/SandpackRoot.tsx
+++ b/src/components/MDX/Sandpack/SandpackRoot.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts
index 07bdcd377..193b07be8 100644
--- a/src/components/MDX/Sandpack/createFileMap.ts
+++ b/src/components/MDX/Sandpack/createFileMap.ts
@@ -12,22 +12,19 @@ export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];
export const createFileMap = (codeSnippets: any) => {
return codeSnippets.reduce(
(result: Record, 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 & {
- meta?: string;
- 'data-mdx-name'?: string;
- }
+ HTMLAttributes & {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
diff --git a/src/components/MDX/Sandpack/index.tsx b/src/components/MDX/Sandpack/index.tsx
index d90facfe8..6755ba8de 100644
--- a/src/components/MDX/Sandpack/index.tsx
+++ b/src/components/MDX/Sandpack/index.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/SandpackWithHTMLOutput.tsx b/src/components/MDX/SandpackWithHTMLOutput.tsx
index 041d7bf9b..51ce28dc1 100644
--- a/src/components/MDX/SandpackWithHTMLOutput.tsx
+++ b/src/components/MDX/SandpackWithHTMLOutput.tsx
@@ -1,5 +1,3 @@
-'use client';
-
import {Children, memo} from 'react';
import InlineCode from './InlineCode';
import Sandpack from './Sandpack';
diff --git a/src/components/MDX/TerminalBlock.tsx b/src/components/MDX/TerminalBlock.tsx
index 73a102167..475292716 100644
--- a/src/components/MDX/TerminalBlock.tsx
+++ b/src/components/MDX/TerminalBlock.tsx
@@ -1,5 +1,3 @@
-'use client';
-
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/TocContext.tsx b/src/components/MDX/TocContext.tsx
index cc7080a8b..8aeead370 100644
--- a/src/components/MDX/TocContext.tsx
+++ b/src/components/MDX/TocContext.tsx
@@ -7,7 +7,7 @@ import type {ReactNode} from 'react';
export type TocItem = {
url: string;
- node: ReactNode;
+ text: ReactNode;
depth: number;
};
export type Toc = Array;
diff --git a/src/components/SafariScrollHandler.tsx b/src/components/SafariScrollHandler.tsx
deleted file mode 100644
index 2cb3e4037..000000000
--- a/src/components/SafariScrollHandler.tsx
+++ /dev/null
@@ -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;
-}
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index 3ff5c1881..c7401487b 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -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 (
<>
@@ -128,7 +127,7 @@ export function Search({
onClose={onClose}
navigator={{
navigate({itemUrl}: any) {
- router.push(itemUrl);
+ Router.push(itemUrl);
},
}}
transformItems={(items: any[]) => {
diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx
new file mode 100644
index 000000000..628085744
--- /dev/null
+++ b/src/components/Seo.tsx
@@ -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;
+ 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 (
+
+
+ {title != null && {pageTitle} }
+ {isHomePage && (
+ // Let Google figure out a good description for each page.
+
+ )}
+
+
+ {finishedTranslations.map((languageCode) => (
+
+ ))}
+
+
+
+ {title != null && (
+
+ )}
+ {description != null && (
+
+ )}
+
+
+
+
+ {title != null && (
+
+ )}
+ {description != null && (
+
+ )}
+
+
+ {searchOrder != null && (
+
+ )}
+
+
+
+
+
+
+
+
+ {children}
+
+ );
+ }
+);
diff --git a/src/components/ThemeScript.jsx b/src/components/ThemeScript.jsx
deleted file mode 100644
index 66034557c..000000000
--- a/src/components/ThemeScript.jsx
+++ /dev/null
@@ -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 (
-
- );
-}
diff --git a/src/components/UwuScript.jsx b/src/components/UwuScript.jsx
deleted file mode 100644
index d2c26585d..000000000
--- a/src/components/UwuScript.jsx
+++ /dev/null
@@ -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 (
-
- );
-}
diff --git a/src/content/community/docs-contributors.md b/src/content/community/docs-contributors.md
index 4f4567846..27b32a18f 100644
--- a/src/content/community/docs-contributors.md
+++ b/src/content/community/docs-contributors.md
@@ -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.
diff --git a/src/content/versions.md b/src/content/versions.md
index 8956e80e8..8530f6324 100644
--- a/src/content/versions.md
+++ b/src/content/versions.md
@@ -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:
-VIDEO
+VIDEO
diff --git a/src/hooks/usePendingRoute.ts b/src/hooks/usePendingRoute.ts
index 2e3f7c6f6..229a36e64 100644
--- a/src/hooks/usePendingRoute.ts
+++ b/src/hooks/usePendingRoute.ts
@@ -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(null);
+ const currentRoute = useRef(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;
diff --git a/src/instrumentation.js b/src/instrumentation.js
deleted file mode 100644
index 94c964cea..000000000
--- a/src/instrumentation.js
+++ /dev/null
@@ -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();
- });
- }
-}
diff --git a/src/app/not-found.tsx b/src/pages/404.js
similarity index 60%
rename from src/app/not-found.tsx
rename to src/pages/404.js
index 1ea86a106..2a88fc29a 100644
--- a/src/app/not-found.tsx
+++ b/src/pages/404.js
@@ -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 (
-
+
This page doesn’t exist.
@@ -34,11 +27,3 @@ export default function NotFound() {
);
}
-
-export async function generateMetadata({}: {}) {
- return generateSeoMetadata({
- title: 'Not Found',
- isHomePage: false,
- path: '/404',
- });
-}
diff --git a/src/app/error.tsx b/src/pages/500.js
similarity index 61%
rename from src/app/error.tsx
rename to src/pages/500.js
index 31725c447..b043e35b2 100644
--- a/src/app/error.tsx
+++ b/src/pages/500.js
@@ -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 (
@@ -35,11 +29,3 @@ export default function Error() {
);
}
-
-export async function generateMetadata({}: {}) {
- return generateSeoMetadata({
- title: 'Something Went Wrong',
- isHomePage: false,
- path: '/500',
- });
-}
diff --git a/src/pages/[[...markdownPath]].js b/src/pages/[[...markdownPath]].js
new file mode 100644
index 000000000..bef4508df
--- /dev/null
+++ b/src/pages/[[...markdownPath]].js
@@ -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 (
+
+ {parsedContent}
+
+ );
+}
+
+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 ;
+ } 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,
+ };
+}
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
new file mode 100644
index 000000000..5431f87cc
--- /dev/null
+++ b/src/pages/_app.tsx
@@ -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 ;
+}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
new file mode 100644
index 000000000..6849df35d
--- /dev/null
+++ b/src/pages/_document.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MyDocument;
diff --git a/src/pages/errors/[errorCode].tsx b/src/pages/errors/[errorCode].tsx
new file mode 100644
index 000000000..de9eab5bb
--- /dev/null
+++ b/src/pages/errors/[errorCode].tsx
@@ -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) {
+ const parsedContent = useMemo(
+ () => JSON.parse(content, reviveNodeOnClient),
+ [content]
+ );
+
+ return (
+
+
+ {parsedContent}
+ {/*
+
+ 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.
+
+
+ */}
+
+
+ );
+}
+
+// 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 ;
+ } 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 | null = null;
+
+export const getStaticProps: GetStaticProps = 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',
+ };
+};
diff --git a/src/pages/errors/index.tsx b/src/pages/errors/index.tsx
new file mode 100644
index 000000000..d7742f03a
--- /dev/null
+++ b/src/pages/errors/index.tsx
@@ -0,0 +1,3 @@
+import ErrorDecoderPage from './[errorCode]';
+export default ErrorDecoderPage;
+export {getStaticProps} from './[errorCode]';
diff --git a/src/styles/index.css b/src/styles/index.css
index 28d8473ac..281111092 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -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;
diff --git a/src/utils/compileMDX.ts b/src/utils/compileMDX.ts
new file mode 100644
index 000000000..be770c29a
--- /dev/null
+++ b/src/utils/compileMDX.ts
@@ -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 | 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;
+}
diff --git a/src/utils/generateMDX.tsx b/src/utils/generateMDX.tsx
deleted file mode 100644
index 4d24e5f7a..000000000
--- a/src/utils/generateMDX.tsx
+++ /dev/null
@@ -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 {
- 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 {
- 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 {
- 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: ,
- 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: ,
- toc: tocWithMDX,
- meta: result.meta,
- };
-}
-
-async function getTransformedToc(toc: ExtractedTOC[]): Promise {
- 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 = ;
-
- return item;
- })
- );
-}
diff --git a/src/utils/generateMetadata.ts b/src/utils/generateMetadata.ts
deleted file mode 100644
index e45b10dab..000000000
--- a/src/utils/generateMetadata.ts
+++ /dev/null
@@ -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(),
- }),
- },
- };
-}
diff --git a/src/utils/mdx/MaxWidthWrapperPlugin.ts b/src/utils/mdx/MaxWidthWrapperPlugin.ts
deleted file mode 100644
index 22f1a022f..000000000
--- a/src/utils/mdx/MaxWidthWrapperPlugin.ts
+++ /dev/null
@@ -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;
- };
-}
diff --git a/src/utils/mdx/MetaAttributesPlugin.ts b/src/utils/mdx/MetaAttributesPlugin.ts
deleted file mode 100644
index f708fd368..000000000
--- a/src/utils/mdx/MetaAttributesPlugin.ts
+++ /dev/null
@@ -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;
- }
- });
- };
-}
diff --git a/src/utils/mdx/TOCExtractorPlugin.ts b/src/utils/mdx/TOCExtractorPlugin.ts
deleted file mode 100644
index dd8e4d644..000000000
--- a/src/utils/mdx/TOCExtractorPlugin.ts
+++ /dev/null
@@ -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., )
- 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;
- };
-}
diff --git a/src/utils/prepareMDX.js b/src/utils/prepareMDX.js
new file mode 100644
index 000000000..20a22577d
--- /dev/null
+++ b/src/utils/prepareMDX.js
@@ -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
+ // 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({wrapQueue} );
+ 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);
+ }
+ }
+}
diff --git a/src/utils/rss.js b/src/utils/rss.js
new file mode 100644
index 000000000..29e5511ea
--- /dev/null
+++ b/src/utils/rss.js
@@ -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}));
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index f709aba7d..f31a24516 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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',
diff --git a/tsconfig.json b/tsconfig.json
index 983db114c..440b38510 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"
+ ]
}
diff --git a/types.d.ts b/types.d.ts
deleted file mode 100644
index 72e9cbfa1..000000000
--- a/types.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-declare module 'metro-cache' {
- export class FileStore {
- constructor(options: {root: string});
- get(hash: Buffer): Promise;
- set(hash: Buffer, value: Buffer): Promise;
- }
- export function stableHash(obj: any): string;
-}
diff --git a/yarn.lock b/yarn.lock
index 2e33a440c..a118cbeda 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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-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-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-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-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-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-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-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-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.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.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.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.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.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.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.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"
From 6fc98fffdaad3b84e6093d1eb8def8f2cedeee16 Mon Sep 17 00:00:00 2001
From: Karl Horky
Date: Sun, 2 Feb 2025 19:19:49 +0100
Subject: [PATCH 109/109] Fix Vite URLs (#7462)
* Fix Vite link
* Fix additional vitejs.dev URLs to use vite.dev
---
src/content/learn/add-react-to-an-existing-project.md | 4 ++--
src/content/learn/start-a-new-react-project.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/content/learn/add-react-to-an-existing-project.md b/src/content/learn/add-react-to-an-existing-project.md
index 8e468452e..4684fb5b0 100644
--- a/src/content/learn/add-react-to-an-existing-project.md
+++ b/src/content/learn/add-react-to-an-existing-project.md
@@ -45,7 +45,7 @@ A modular JavaScript environment lets you write your React components in individ
* **If your app is already split into files that use `import` statements,** try to use the setup you already have. Check whether writing `
` in your JS code causes a syntax error. If it causes a syntax error, you might need to [transform your JavaScript code with Babel](https://babeljs.io/setup), and enable the [Babel React preset](https://babeljs.io/docs/babel-preset-react) to use JSX.
-* **If your app doesn't have an existing setup for compiling JavaScript modules,** set it up with [Vite](https://vitejs.dev/). The Vite community maintains [many integrations with backend frameworks](https://github.com/vitejs/awesome-vite#integrations-with-backends), including Rails, Django, and Laravel. If your backend framework is not listed, [follow this guide](https://vitejs.dev/guide/backend-integration.html) to manually integrate Vite builds with your backend.
+* **If your app doesn't have an existing setup for compiling JavaScript modules,** set it up with [Vite](https://vite.dev/). The Vite community maintains [many integrations with backend frameworks](https://github.com/vitejs/awesome-vite#integrations-with-backends), including Rails, Django, and Laravel. If your backend framework is not listed, [follow this guide](https://vite.dev/guide/backend-integration.html) to manually integrate Vite builds with your backend.
To check whether your setup works, run this command in your project folder:
@@ -85,7 +85,7 @@ If the entire content of your page was replaced by a "Hello, world!", everything
-Integrating a modular JavaScript environment into an existing project for the first time can feel intimidating, but it's worth it! If you get stuck, try our [community resources](/community) or the [Vite Chat](https://chat.vitejs.dev/).
+Integrating a modular JavaScript environment into an existing project for the first time can feel intimidating, but it's worth it! If you get stuck, try our [community resources](/community) or the [Vite Chat](https://chat.vite.dev/).
diff --git a/src/content/learn/start-a-new-react-project.md b/src/content/learn/start-a-new-react-project.md
index bd5ba6c50..1eb263618 100644
--- a/src/content/learn/start-a-new-react-project.md
+++ b/src/content/learn/start-a-new-react-project.md
@@ -27,7 +27,7 @@ Even if you don't need routing or data fetching at first, you'll likely want to
**React frameworks on this page solve problems like these by default, with no extra work from your side.** They let you start very lean and then scale your app with your needs. Each React framework has a community, so finding answers to questions and upgrading tooling is easier. Frameworks also give structure to your code, helping you and others retain context and skills between different projects. Conversely, with a custom setup it's easier to get stuck on unsupported dependency versions, and you'll essentially end up creating your own framework—albeit one with no community or upgrade path (and if it's anything like the ones we've made in the past, more haphazardly designed).
-If your app has unusual constraints not served well by these frameworks, or you prefer to solve these problems yourself, you can roll your own custom setup with React. Grab `react` and `react-dom` from npm, set up your custom build process with a bundler like [Vite](https://vitejs.dev/) or [Parcel](https://parceljs.org/), and add other tools as you need them for routing, static generation or server-side rendering, and more.
+If your app has unusual constraints not served well by these frameworks, or you prefer to solve these problems yourself, you can roll your own custom setup with React. Grab `react` and `react-dom` from npm, set up your custom build process with a bundler like [Vite](https://vite.dev/) or [Parcel](https://parceljs.org/), and add other tools as you need them for routing, static generation or server-side rendering, and more.