From 7e62576c425ba4872f145f6c2d96dc6d730f286c Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 13:08:38 +0100
Subject: [PATCH 01/39] update version to latest + move folders around
---
next-env.d.ts | 2 +-
package.json | 2 +-
src/{pages => _pages}/404.js | 0
src/{pages => _pages}/500.js | 0
src/{pages => _pages}/[[...markdownPath]].js | 0
src/{pages => _pages}/_app.tsx | 0
src/{pages => _pages}/_document.tsx | 0
src/{pages => _pages}/errors/[errorCode].tsx | 0
src/{pages => _pages}/errors/index.tsx | 0
src/app/[[...markdownPath]]/page.js | 179 +++++++++++++++++++
src/app/_app.tsx | 58 ++++++
src/app/layout.tsx | 158 ++++++++++++++++
tsconfig.json | 10 +-
yarn.lock | 112 ++++++------
14 files changed, 461 insertions(+), 60 deletions(-)
rename src/{pages => _pages}/404.js (100%)
rename src/{pages => _pages}/500.js (100%)
rename src/{pages => _pages}/[[...markdownPath]].js (100%)
rename src/{pages => _pages}/_app.tsx (100%)
rename src/{pages => _pages}/_document.tsx (100%)
rename src/{pages => _pages}/errors/[errorCode].tsx (100%)
rename src/{pages => _pages}/errors/index.tsx (100%)
create mode 100644 src/app/[[...markdownPath]]/page.js
create mode 100644 src/app/_app.tsx
create mode 100644 src/app/layout.tsx
diff --git a/next-env.d.ts b/next-env.d.ts
index 52e831b4342..1b3be0840f3 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/pages/api-reference/config/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package.json b/package.json
index 6d6b53f92de..8613226a387 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"date-fns": "^2.16.1",
"debounce": "^1.2.1",
"github-slugger": "^1.3.0",
- "next": "15.1.0",
+ "next": "^15.1.5",
"next-remote-watch": "^1.0.0",
"parse-numeric-range": "^1.2.0",
"react": "^19.0.0",
diff --git a/src/pages/404.js b/src/_pages/404.js
similarity index 100%
rename from src/pages/404.js
rename to src/_pages/404.js
diff --git a/src/pages/500.js b/src/_pages/500.js
similarity index 100%
rename from src/pages/500.js
rename to src/_pages/500.js
diff --git a/src/pages/[[...markdownPath]].js b/src/_pages/[[...markdownPath]].js
similarity index 100%
rename from src/pages/[[...markdownPath]].js
rename to src/_pages/[[...markdownPath]].js
diff --git a/src/pages/_app.tsx b/src/_pages/_app.tsx
similarity index 100%
rename from src/pages/_app.tsx
rename to src/_pages/_app.tsx
diff --git a/src/pages/_document.tsx b/src/_pages/_document.tsx
similarity index 100%
rename from src/pages/_document.tsx
rename to src/_pages/_document.tsx
diff --git a/src/pages/errors/[errorCode].tsx b/src/_pages/errors/[errorCode].tsx
similarity index 100%
rename from src/pages/errors/[errorCode].tsx
rename to src/_pages/errors/[errorCode].tsx
diff --git a/src/pages/errors/index.tsx b/src/_pages/errors/index.tsx
similarity index 100%
rename from src/pages/errors/index.tsx
rename to src/_pages/errors/index.tsx
diff --git a/src/app/[[...markdownPath]]/page.js b/src/app/[[...markdownPath]]/page.js
new file mode 100644
index 00000000000..bef4508df06
--- /dev/null
+++ b/src/app/[[...markdownPath]]/page.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/app/_app.tsx b/src/app/_app.tsx
new file mode 100644
index 00000000000..5431f87cc9e
--- /dev/null
+++ b/src/app/_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/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 00000000000..6849df35d6a
--- /dev/null
+++ b/src/app/layout.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/tsconfig.json b/tsconfig.json
index 440b38510fa..9d72e01bc55 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,12 +22,18 @@
"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"
],
"exclude": [
"node_modules"
diff --git a/yarn.lock b/yarn.lock
index a118cbeda5f..985ad4ef565 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.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.5.tgz#2a87f0be705d40b7bd1bf8c66581edc32dcd2b1f"
+ integrity sha512-jg8ygVq99W3/XXb9Y6UQsritwhjc+qeiO7QrGZRYOfviyr/HcdnhdBQu4gbp2rBIh2ZyBYTBMWbPw3JSCb0GHw==
"@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.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.5.tgz#2b59120c1576b718ed4695508c2a9d4a3248ead7"
+ integrity sha512-5ttHGE75Nw9/l5S8zR2xEwR8OHEqcpPym3idIMAZ2yo+Edk0W/Vf46jGqPOZDk+m/SJ+vYZDSuztzhVha8rcdA==
+
+"@next/swc-darwin-x64@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.5.tgz#362fa82504ee496af0e7364d8383585f504a03de"
+ integrity sha512-8YnZn7vDURUUTInfOcU5l0UWplZGBqUlzvqKKUFceM11SzfNEz7E28E1Arn4/FsOf90b1Nopboy7i7ufc4jXag==
+
+"@next/swc-linux-arm64-gnu@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.5.tgz#34bd054f222f45d7b3f5887cc2be3fe87b69e6a5"
+ integrity sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==
+
+"@next/swc-linux-arm64-musl@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.5.tgz#125356cf3e0a9c90e32383e462d14790a6a40763"
+ integrity sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==
+
+"@next/swc-linux-x64-gnu@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.5.tgz#1dc8bf2374b5f3384370ef97e4b3bbead73b250c"
+ integrity sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==
+
+"@next/swc-linux-x64-musl@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.5.tgz#45dc68da9e1f22de4ef37d397aeb9cf40746871d"
+ integrity sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==
+
+"@next/swc-win32-arm64-msvc@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.5.tgz#ce51388f07b9f0f0e1d4f22dcbba75bcca1952fa"
+ integrity sha512-HPULzqR/VqryQZbZME8HJE3jNFmTGcp+uRMHabFbQl63TtDPm+oCXAz3q8XyGv2AoihwNApVlur9Up7rXWRcjg==
+
+"@next/swc-win32-x64-msvc@15.1.5":
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.5.tgz#43135b3d9705ae8e6a6b90c8bdf4dc02b9ffc90c"
+ integrity sha512-n74fUb/Ka1dZSVYfjwQ+nSJ+ifUff7jGurFcTuJNKZmI62FFOxQXUYit/uZXPTj2cirm1rvGWHG2GhbSol5Ikw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -5798,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.1.0:
- version "15.1.0"
- resolved "https://registry.yarnpkg.com/next/-/next-15.1.0.tgz#be847cf67ac94ae23b57f3ea6d10642f3fc1ad69"
- integrity sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==
+next@^15.1.5:
+ version "15.1.5"
+ resolved "https://registry.yarnpkg.com/next/-/next-15.1.5.tgz#80972a606d17ead86323d3405abf3bd3e2c894c8"
+ integrity sha512-Cf/TEegnt01hn3Hoywh6N8fvkhbOuChO4wFje24+a86wKOubgVaWkDqxGVgoWlz2Hp9luMJ9zw3epftujdnUOg==
dependencies:
- "@next/env" "15.1.0"
+ "@next/env" "15.1.5"
"@swc/counter" "0.1.3"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
@@ -5811,14 +5811,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.1.5"
+ "@next/swc-darwin-x64" "15.1.5"
+ "@next/swc-linux-arm64-gnu" "15.1.5"
+ "@next/swc-linux-arm64-musl" "15.1.5"
+ "@next/swc-linux-x64-gnu" "15.1.5"
+ "@next/swc-linux-x64-musl" "15.1.5"
+ "@next/swc-win32-arm64-msvc" "15.1.5"
+ "@next/swc-win32-x64-msvc" "15.1.5"
sharp "^0.33.5"
nice-try@^1.0.4:
From afc2f86bf31f4e9ca03ba8c3d949bf8af8908f63 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 14:16:27 +0100
Subject: [PATCH 02/39] getting home page working
---
next.config.js | 4 +
src/app/[[...markdownPath]]/page.js | 212 +++++-------
src/app/layout.tsx | 315 ++++++++++--------
src/components/Analytics.tsx | 59 ++++
src/components/Layout/Feedback.tsx | 6 +-
src/components/Layout/Page.tsx | 38 +--
.../Layout/Sidebar/SidebarRouteTree.tsx | 4 +-
src/components/Layout/TopNav/TopNav.tsx | 6 +-
src/components/MDX/Challenges/Challenges.tsx | 8 +-
src/components/MDX/ExpandableExample.tsx | 7 +-
src/components/MDX/MDXComponents.tsx | 2 +
src/components/SafariScrollHandler.tsx | 22 ++
src/components/Seo.tsx | 245 +++++---------
src/hooks/usePendingRoute.ts | 3 +-
14 files changed, 461 insertions(+), 470 deletions(-)
create mode 100644 src/components/Analytics.tsx
create mode 100644 src/components/SafariScrollHandler.tsx
diff --git a/next.config.js b/next.config.js
index 861792c8e53..0bd2efdd034 100644
--- a/next.config.js
+++ b/next.config.js
@@ -13,6 +13,10 @@ const nextConfig = {
reactCompiler: true,
},
env: {},
+ serverExternalPackages: [
+ '@babel/core',
+ '@babel/plugin-transform-modules-commonjs',
+ ],
webpack: (config, {dev, isServer, ...options}) => {
if (process.env.ANALYZE) {
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
diff --git a/src/app/[[...markdownPath]]/page.js b/src/app/[[...markdownPath]]/page.js
index bef4508df06..7e602bd4f78 100644
--- a/src/app/[[...markdownPath]]/page.js
+++ b/src/app/[[...markdownPath]]/page.js
@@ -1,83 +1,24 @@
-/*
- * Copyright (c) Facebook, Inc. and its affiliates.
- */
-
-import {Fragment, useMemo} from 'react';
-import {useRouter} from 'next/router';
+// src/app/[[...markdownPath]]/page.js
+import {Fragment} from 'react';
+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 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';
- }
-}
+import {generateRssFeed} from '../../utils/rss';
// 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.
+ key = parentPropertyName;
}
let props = val[3];
if (Type === 'wrapper') {
@@ -97,62 +38,77 @@ function reviveNodeOnClient(parentPropertyName, val) {
}
}
-// Put MDX output into JSON for client.
-export async function getStaticProps(context) {
- generateRssFeed();
- const fs = require('fs');
- const rootDir = process.cwd() + '/src/content/';
+function getActiveSection(pathname) {
+ 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) {
+ switch (section) {
+ case 'home':
+ case 'unknown':
+ return sidebarHome;
+ case 'learn':
+ return sidebarLearn;
+ case 'reference':
+ return sidebarReference;
+ case 'community':
+ return sidebarCommunity;
+ case 'blog':
+ return sidebarBlog;
+ }
+}
- // Read MDX from the file.
- let path = (context.params.markdownPath || []).join('/') || 'index';
+// This replaces getStaticProps
+async function getPageContent(markdownPath) {
+ const rootDir = path.join(process.cwd(), 'src/content');
+ let mdxPath = markdownPath?.join('/') || 'index';
let mdx;
+
try {
- mdx = fs.readFileSync(rootDir + path + '.md', 'utf8');
+ mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
} catch {
- mdx = fs.readFileSync(rootDir + path + '/index.md', 'utf8');
+ mdx = await fs.readFile(path.join(rootDir, mdxPath, 'index.md'), 'utf8');
}
- const {toc, content, meta, languages} = await compileMDX(mdx, path, {});
- return {
- props: {
- toc,
- content,
- meta,
- languages,
- },
- };
+ // Generate RSS feed during build time
+ if (process.env.NODE_ENV === 'production') {
+ await generateRssFeed();
+ }
+
+ return await compileMDX(mdx, mdxPath, {});
}
-// 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';
+// This replaces getStaticPaths
+export async function generateStaticParams() {
+ const rootDir = path.join(process.cwd(), 'src/content');
- // Find all MD files recursively.
async function getFiles(dir) {
- const subdirs = await readdir(dir);
+ const entries = await fs.readdir(dir, {withFileTypes: true});
const files = await Promise.all(
- subdirs.map(async (subdir) => {
- const res = resolve(dir, subdir);
- return (await stat(res)).isDirectory()
+ entries.map(async (entry) => {
+ const res = path.resolve(dir, entry.name);
+ return entry.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/'))
- );
+ return files
+ .flat()
+ .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') {
@@ -163,17 +119,33 @@ export async function getStaticPaths() {
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 files.map((file) => ({
+ markdownPath: getSegments(file),
}));
+}
+
+export default async function WrapperPage({params}) {
+ const {markdownPath} = params;
+ const {content, toc, meta, languages} = await getPageContent(markdownPath);
+
+ const pathname = '/' + (markdownPath?.join('/') || '');
+ const section = getActiveSection(pathname);
+ const routeTree = await getRouteTree(section);
- return {
- paths: paths,
- fallback: false,
- };
+ const parsedContent = JSON.parse(content, reviveNodeOnClient);
+ const parsedToc = JSON.parse(toc, reviveNodeOnClient);
+
+ return (
+
+ {parsedContent}
+
+ );
}
+
+// Configure dynamic segments to be statically generated
+export const dynamicParams = false;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6849df35d6a..3ff01cabcb4 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,158 +1,181 @@
-/*
- * Copyright (c) Facebook, Inc. and its affiliates.
- */
-
-import {Html, Head, Main, NextScript} from 'next/document';
+// app/layout.jsx
import {siteConfig} from '../siteConfig';
+import Script from 'next/script';
+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 {generateMetadata as generateSeoMetadata} from 'components/Seo';
+
+export async function generateMetadata({params}) {
+ const metadata = generateSeoMetadata({
+ title: 'React',
+ isHomePage: true,
+ path: '/',
+ });
+
+ return {
+ ...metadata,
+ 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',
+ themeColor: [
+ {media: '(prefers-color-scheme: light)', color: '#23272f'},
+ {media: '(prefers-color-scheme: dark)', color: '#23272f'},
+ ],
+ other: {
+ 'msapplication-TileColor': '#2b5797',
+ },
+ };
+}
-const MyDocument = () => {
+function ThemeScript() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ );
+}
- darkQuery.addEventListener('change', function (e) {
- if (!preferredTheme) {
- setTheme(e.matches ? 'dark' : 'light');
+function UwuScript() {
+ return (
+
+ );
+}
- // Detect whether the browser is Mac to display platform specific content
- // An example of such content can be the keyboard shortcut displayed in the search bar
- document.documentElement.classList.add(
- window.navigator.platform.includes('Mac')
- ? "platform-mac"
- : "platform-win"
- );
- })();
- `,
- }}
- />
-
-
+export default function RootLayout({children}) {
+ return (
+
+
+
+
+
+
+ {children}
-
+
);
-};
-
-export default MyDocument;
+}
diff --git a/src/components/Analytics.tsx b/src/components/Analytics.tsx
new file mode 100644
index 00000000000..f791f9e3604
--- /dev/null
+++ b/src/components/Analytics.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import {useEffect} from 'react';
+import Script from 'next/script';
+import {usePathname, useSearchParams} from 'next/navigation';
+
+declare global {
+ interface Window {
+ gtag: (...args: any[]) => void;
+ }
+}
+
+export function Analytics() {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ 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 (pathname && window.gtag) {
+ const cleanedUrl = `${pathname}${
+ searchParams?.toString() ? `?${searchParams.toString()}` : ''
+ }`.split(/[\?\#]/)[0];
+ window.gtag('event', 'pageview', {
+ event_label: cleanedUrl,
+ });
+ }
+ }, [pathname, searchParams]);
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/Layout/Feedback.tsx b/src/components/Layout/Feedback.tsx
index 34db728ced2..16b974c10dd 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 24d379589de..4b280d99e9e 100644
--- a/src/components/Layout/Page.tsx
+++ b/src/components/Layout/Page.tsx
@@ -1,14 +1,15 @@
+'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 {usePathname, useSearchParams} from 'next/navigation';
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';
@@ -20,8 +21,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 {
@@ -46,8 +47,9 @@ export function Page({
section,
languages = null,
}: PageProps) {
- const {asPath} = useRouter();
- const cleanedPath = asPath.split(/[\?\#]/)[0];
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const cleanedPath = pathname.split(/[\?\#]/)[0];
const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta(
cleanedPath,
routeTree
@@ -120,24 +122,22 @@ export function Page({
return (
<>
-
+ /> */}
{(isHomePage || isBlogIndex) && (
-
-
-
+ // RSS Feed link is now handled by metadata in layout.tsx
+
)}
- {/* */}
+ key={pathname + searchParams.toString()}>
{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 72003df74f2..f67b0ed2b41 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/TopNav/TopNav.tsx b/src/components/Layout/TopNav/TopNav.tsx
index cc5c654e3d0..f8e9023fd87 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/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx
index 21fc6865c02..ac72f99b715 100644
--- a/src/components/MDX/Challenges/Challenges.tsx
+++ b/src/components/MDX/Challenges/Challenges.tsx
@@ -9,7 +9,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[];
@@ -90,12 +90,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 +112,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/ExpandableExample.tsx b/src/components/MDX/ExpandableExample.tsx
index 1e709e4839c..bb9ffa6cb56 100644
--- a/src/components/MDX/ExpandableExample.tsx
+++ b/src/components/MDX/ExpandableExample.tsx
@@ -9,8 +9,8 @@ import {IconDeepDive} from '../Icon/IconDeepDive';
import {IconCodeBlock} from '../Icon/IconCodeBlock';
import {Button} from '../Button';
import {H4} from './Heading';
-import {useRouter} from 'next/router';
import {useEffect, useRef, useState} from 'react';
+import {usePathname} from 'next/navigation';
interface ExpandableExampleProps {
children: React.ReactNode;
@@ -28,8 +28,9 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
const isExample = type === 'Example';
const id = children[0].props.id;
- const {asPath} = useRouter();
- const shouldAutoExpand = id === asPath.split('#')[1];
+ const pathname = usePathname();
+
+ const shouldAutoExpand = id === pathname.split('#')[1];
const queuedExpandRef = useRef(shouldAutoExpand);
const [isExpanded, setIsExpanded] = useState(false);
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index f24fac5988e..3452223b20b 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/SafariScrollHandler.tsx b/src/components/SafariScrollHandler.tsx
new file mode 100644
index 00000000000..2cb3e4037a3
--- /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/Seo.tsx b/src/components/Seo.tsx
index 628085744d7..7fb92d03236 100644
--- a/src/components/Seo.tsx
+++ b/src/components/Seo.tsx
@@ -1,185 +1,92 @@
-/*
- * Copyright (c) Facebook, Inc. and its affiliates.
- */
-
-import * as React from 'react';
-import Head from 'next/head';
-import {withRouter, Router} from 'next/router';
+// lib/seo.ts
+import {Metadata} from 'next';
import {siteConfig} from '../siteConfig';
import {finishedTranslations} from 'utils/finishedTranslations';
export interface SeoProps {
title: string;
- titleForTitleTag: undefined | string;
+ titleForTitleTag?: string;
description?: string;
image?: string;
- // jsonld?: JsonLDType | Array;
- children?: React.ReactNode;
isHomePage: boolean;
searchOrder?: number;
+ path: string;
}
-// 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}
-
- );
- }
-);
+export function generateMetadata({
+ title,
+ titleForTitleTag,
+ image = '/images/og-default.png',
+ isHomePage,
+ description: customDescription,
+ searchOrder,
+ path,
+}: SeoProps): Metadata {
+ const siteDomain = getDomain(siteConfig.languageCode);
+ const canonicalUrl = `https://${siteDomain}${path.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, '');
+
+ const 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.'
+ : 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)),
+ ])
+ ),
+ };
+
+ const metadata: Metadata = {
+ title: pageTitle,
+ description: isHomePage ? description : undefined,
+ alternates: {
+ canonical: canonicalUrl,
+ languages: alternateLanguages,
+ },
+ openGraph: {
+ title: pageTitle,
+ description,
+ url: canonicalUrl,
+ siteName: 'React',
+ type: 'website',
+ images: [
+ {
+ url: `https://${siteDomain}${image}`,
+ },
+ ],
+ },
+ twitter: {
+ card: 'summary_large_image',
+ site: '@reactjs',
+ creator: '@reactjs',
+ title: twitterTitle,
+ description,
+ images: [`https://${siteDomain}${image}`],
+ },
+ verification: {
+ google: 'sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0',
+ },
+ other: {
+ 'fb:app_id': '623268441017527',
+ ...(searchOrder != null && {
+ 'algolia-search-order': searchOrder.toString(),
+ }),
+ },
+ };
+
+ return metadata;
+}
diff --git a/src/hooks/usePendingRoute.ts b/src/hooks/usePendingRoute.ts
index 229a36e64c4..31b8bd0a622 100644
--- a/src/hooks/usePendingRoute.ts
+++ b/src/hooks/usePendingRoute.ts
@@ -2,10 +2,11 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
-import {useRouter} from 'next/router';
+// import {useRouter} from 'next/router';
import {useState, useRef, useEffect} from 'react';
const usePendingRoute = () => {
+ return null;
const {events} = useRouter();
const [pendingRoute, setPendingRoute] = useState(null);
const currentRoute = useRef(null);
From 82ce63ae66ef5dbafeeee73d0a87735112c69f7d Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 15:48:07 +0100
Subject: [PATCH 03/39] make the mdx setup work
---
src/_pages/errors/[errorCode].tsx | 2 +-
src/app/[[...markdownPath]]/page.js | 14 +-
src/app/layout.tsx | 1 +
src/components/MDX/Challenges/Challenges.tsx | 2 +
src/components/MDX/CodeBlock/CodeBlock.tsx | 2 +
src/components/MDX/CodeBlock/index.tsx | 2 +
src/components/MDX/ErrorDecoder.tsx | 2 +
src/components/MDX/ExpandableExample.tsx | 2 +
src/components/MDX/LanguagesContext.tsx | 2 +
src/components/MDX/MDXComponents.tsx | 232 +++++++++---------
src/components/MDX/Sandpack/CustomPreset.tsx | 2 +
src/components/MDX/Sandpack/SandpackRoot.tsx | 2 +
src/components/MDX/Sandpack/index.tsx | 2 +
src/components/MDX/SandpackWithHTMLOutput.tsx | 2 +
src/components/MDX/TerminalBlock.tsx | 2 +
src/utils/compileMDX.ts | 168 -------------
src/utils/compileMDX.tsx | 71 ++++++
src/utils/prepareMDX.js | 2 +-
18 files changed, 218 insertions(+), 294 deletions(-)
delete mode 100644 src/utils/compileMDX.ts
create mode 100644 src/utils/compileMDX.tsx
diff --git a/src/_pages/errors/[errorCode].tsx b/src/_pages/errors/[errorCode].tsx
index de9eab5bb56..127c5e00a26 100644
--- a/src/_pages/errors/[errorCode].tsx
+++ b/src/_pages/errors/[errorCode].tsx
@@ -1,6 +1,6 @@
import {Fragment, useMemo} from 'react';
import {Page} from 'components/Layout/Page';
-import {MDXComponents} from 'components/MDX/MDXComponents';
+import {MDXComponents} from 'components/MDX/MDXComponentsWrapper';
import sidebarLearn from 'sidebarLearn.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next';
diff --git a/src/app/[[...markdownPath]]/page.js b/src/app/[[...markdownPath]]/page.js
index 7e602bd4f78..701dbab191e 100644
--- a/src/app/[[...markdownPath]]/page.js
+++ b/src/app/[[...markdownPath]]/page.js
@@ -123,29 +123,27 @@ export async function generateStaticParams() {
markdownPath: getSegments(file),
}));
}
-
export default async function WrapperPage({params}) {
- const {markdownPath} = params;
+ const {markdownPath} = await params;
+
+ // Get the MDX content and associated data
const {content, toc, meta, languages} = await getPageContent(markdownPath);
const pathname = '/' + (markdownPath?.join('/') || '');
const section = getActiveSection(pathname);
const routeTree = await getRouteTree(section);
- const parsedContent = JSON.parse(content, reviveNodeOnClient);
- const parsedToc = JSON.parse(toc, reviveNodeOnClient);
-
+ // Pass the content and TOC directly, as `getPageContent` should already return them in the correct format
return (
- {parsedContent}
+ {content}
);
}
-
// Configure dynamic segments to be statically generated
export const dynamicParams = false;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 3ff01cabcb4..2344e71d3ab 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -174,6 +174,7 @@ export default function RootLayout({children}) {
+
{children}
diff --git a/src/components/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx
index ac72f99b715..d5f65689b2d 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.
*/
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx
index 1fd9a8a90b1..1d126530f5f 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 551c1d1b668..c06fdbc8120 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/ErrorDecoder.tsx b/src/components/MDX/ErrorDecoder.tsx
index b04fa9f7987..b0f35216265 100644
--- a/src/components/MDX/ErrorDecoder.tsx
+++ b/src/components/MDX/ErrorDecoder.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import {useEffect, useState} from 'react';
import {useErrorDecoderParams} from '../ErrorDecoderContext';
import cn from 'classnames';
diff --git a/src/components/MDX/ExpandableExample.tsx b/src/components/MDX/ExpandableExample.tsx
index bb9ffa6cb56..887a1258c4b 100644
--- a/src/components/MDX/ExpandableExample.tsx
+++ b/src/components/MDX/ExpandableExample.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
diff --git a/src/components/MDX/LanguagesContext.tsx b/src/components/MDX/LanguagesContext.tsx
index 776a11c0d95..719ea4f998c 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/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index 3452223b20b..d18e537fef3 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -1,10 +1,10 @@
-'use client';
+// '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';
@@ -263,99 +263,99 @@ function AuthorCredit({
);
}
-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 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}
- )}
-
-
-
- );
-}
+// 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;
@@ -388,27 +388,27 @@ function calculateNestedToc(toc: Toc): NestedTocRoot {
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 && }
-
- ))}
-
- );
-}
+// 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';
@@ -500,10 +500,10 @@ export const MDXComponents = {
Pitfall,
Deprecated,
Wip,
- Illustration,
- IllustrationBlock,
+ // Illustration,
+ // IllustrationBlock,
Intro,
- InlineToc,
+ // InlineToc,
LanguageList,
LearnMore,
Math,
diff --git a/src/components/MDX/Sandpack/CustomPreset.tsx b/src/components/MDX/Sandpack/CustomPreset.tsx
index 7d6e566d270..f95d3270ac7 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 67f40d0b3b3..1084ea64747 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/index.tsx b/src/components/MDX/Sandpack/index.tsx
index 6755ba8de69..d90facfe80e 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 51ce28dc149..041d7bf9bff 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 47529271619..73a10216712 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/utils/compileMDX.ts b/src/utils/compileMDX.ts
deleted file mode 100644
index be770c29afb..00000000000
--- 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/compileMDX.tsx b/src/utils/compileMDX.tsx
new file mode 100644
index 00000000000..84623fec096
--- /dev/null
+++ b/src/utils/compileMDX.tsx
@@ -0,0 +1,71 @@
+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 {prepareMDX} from './prepareMDX'; // Assuming prepareMDX is modularized
+import {MDXComponents} from '../components/MDX/MDXComponents'; // Assuming MDXComponents is modularized
+
+const DISK_CACHE_BREAKER = 11;
+
+export default async function compileMDX(
+ mdx: string,
+ path: string | string[],
+ params: {[key: string]: any}
+): Promise<{Component: JSX.Element; toc: any; meta: any}> {
+ // Cache setup
+ const store = new FileStore({
+ root: `${process.cwd()}/node_modules/.cache/react-docs-mdx/`,
+ });
+
+ const hash = Buffer.from(
+ stableHash({
+ mdx,
+ ...params,
+ DISK_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/`);
+ }
+
+ // Compile the MDX source code
+ const code = String(
+ await compile(mdx, {
+ remarkPlugins: [...remarkPlugins, remarkGfm, remarkFrontmatter],
+ outputFormat: 'function-body',
+ })
+ );
+
+ // Parse frontmatter for metadata
+ const {data: meta} = grayMatter(mdx);
+
+ // Run the compiled code with the runtime and get the default export
+ const {default: MDXContent} = await run(code, {
+ ...runtime,
+ baseUrl: import.meta.url,
+ });
+
+ // Prepare TOC (you can process toc within the MDX or separately)
+ const {toc} = prepareMDX(MDXContent);
+
+ // Return the ready-to-render React component
+ return {
+ content: , // Replace {} with your custom components if needed
+ toc,
+ meta,
+ };
+}
diff --git a/src/utils/prepareMDX.js b/src/utils/prepareMDX.js
index 20a22577dc4..7277e6b5c29 100644
--- a/src/utils/prepareMDX.js
+++ b/src/utils/prepareMDX.js
@@ -7,7 +7,7 @@ import {Children} from 'react';
// TODO: This logic could be in MDX plugins instead.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-export const PREPARE_MDX_CACHE_BREAKER = 3;
+export const PREPARE_MDX_CACHE_BREAKER = 4;
// !!! IMPORTANT !!! Bump this if you change any logic.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 84e4e75c6f48ceba7c1a639966553649a93faf4a Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 16:28:16 +0100
Subject: [PATCH 04/39] bypass mdxname
---
src/components/MDX/Sandpack/createFileMap.ts | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/components/MDX/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts
index 193b07be825..23f101fe5e3 100644
--- a/src/components/MDX/Sandpack/createFileMap.ts
+++ b/src/components/MDX/Sandpack/createFileMap.ts
@@ -12,12 +12,13 @@ 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
+ // if (
+ // (codeSnippet.type as any).mdxName !== 'pre' &&
+ // codeSnippet.type !== 'pre'
+ // ) {
+ // return result;
+ // }
const {props} = (
codeSnippet.props as PropsWithChildren<{
children: ReactElement<
From 521e0581b690797cb2e391319fe8d6df1938cff9 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 16:36:16 +0100
Subject: [PATCH 05/39] split out mdx components
---
src/components/MDX/Illustration.tsx | 126 +++++++++++++++++++++++++++
src/components/MDX/InlineToc.tsx | 65 ++++++++++++++
src/components/MDX/MDXComponents.tsx | 93 ++------------------
3 files changed, 197 insertions(+), 87 deletions(-)
create mode 100644 src/components/MDX/Illustration.tsx
create mode 100644 src/components/MDX/InlineToc.tsx
diff --git a/src/components/MDX/Illustration.tsx b/src/components/MDX/Illustration.tsx
new file mode 100644
index 00000000000..ea674a865b5
--- /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/InlineToc.tsx b/src/components/MDX/InlineToc.tsx
new file mode 100644
index 00000000000..eac0350262e
--- /dev/null
+++ b/src/components/MDX/InlineToc.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import Link from 'next/link';
+import {HTMLAttributes, useContext, useMemo} from 'react';
+import {Toc, TocContext, TocItem} from './TocContext';
+
+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 ;
+}
+
+const LI = (p: HTMLAttributes) => (
+
+);
+const UL = (p: HTMLAttributes) => (
+
+);
+
+function InlineTocItem({items}: {items: Array}) {
+ return (
+
+ {items.map((node) => (
+
+ {node.item.text}
+ {node.children.length > 0 && }
+
+ ))}
+
+ );
+}
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index d18e537fef3..9e99a4da692 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -31,14 +31,15 @@ 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';
function CodeStep({children, step}: {children: any; step: number}) {
return (
@@ -234,35 +235,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;
// }>({
@@ -357,59 +329,6 @@ const isInBlockTrue = {isInBlock: true};
// );
// }
-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}) {
@@ -500,10 +419,10 @@ export const MDXComponents = {
Pitfall,
Deprecated,
Wip,
- // Illustration,
- // IllustrationBlock,
+ Illustration,
+ IllustrationBlock,
Intro,
- // InlineToc,
+ InlineToc,
LanguageList,
LearnMore,
Math,
From 78eb453d25ee5a36f246ce559f0958856847c117 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 16:56:37 +0100
Subject: [PATCH 06/39] re-add meta mdx logic
---
src/components/MDX/Sandpack/createFileMap.ts | 1 +
src/utils/compileMDX.tsx | 20 ++++++++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/src/components/MDX/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts
index 23f101fe5e3..7fbbe69feea 100644
--- a/src/components/MDX/Sandpack/createFileMap.ts
+++ b/src/components/MDX/Sandpack/createFileMap.ts
@@ -26,6 +26,7 @@ export const createFileMap = (codeSnippets: any) => {
>;
}>
).children;
+
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/utils/compileMDX.tsx b/src/utils/compileMDX.tsx
index 84623fec096..a98a6c4fb5a 100644
--- a/src/utils/compileMDX.tsx
+++ b/src/utils/compileMDX.tsx
@@ -8,6 +8,7 @@ import remarkGfm from 'remark-gfm';
import remarkFrontmatter from 'remark-frontmatter';
import {prepareMDX} from './prepareMDX'; // Assuming prepareMDX is modularized
import {MDXComponents} from '../components/MDX/MDXComponents'; // Assuming MDXComponents is modularized
+import visit from 'unist-util-visit';
const DISK_CACHE_BREAKER = 11;
@@ -46,6 +47,25 @@ export default async function compileMDX(
const code = String(
await compile(mdx, {
remarkPlugins: [...remarkPlugins, remarkGfm, remarkFrontmatter],
+
+ 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;
+ }
+ });
+ };
+ },
+ ],
outputFormat: 'function-body',
})
);
From 227ca33b155350698e60523ca0e01a9108241386 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 17:30:47 +0100
Subject: [PATCH 07/39] replace mdxName usage
---
src/components/MDX/Challenges/Challenges.tsx | 6 +++--
src/components/MDX/Challenges/index.tsx | 2 ++
src/components/MDX/CodeDiagram.tsx | 2 +-
src/components/MDX/ExpandableExample.tsx | 10 +++++---
src/components/MDX/Link.tsx | 2 +-
src/components/MDX/MDXComponents.tsx | 26 ++++++++++++++------
src/components/MDX/PackageImport.tsx | 4 +--
7 files changed, 35 insertions(+), 17 deletions(-)
diff --git a/src/components/MDX/Challenges/Challenges.tsx b/src/components/MDX/Challenges/Challenges.tsx
index d5f65689b2d..ff9586ae67b 100644
--- a/src/components/MDX/Challenges/Challenges.tsx
+++ b/src/components/MDX/Challenges/Challenges.tsx
@@ -42,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;
diff --git a/src/components/MDX/Challenges/index.tsx b/src/components/MDX/Challenges/index.tsx
index 413fd461120..d85f5eb76c3 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/CodeDiagram.tsx b/src/components/MDX/CodeDiagram.tsx
index 2a198fc56a5..41c94efc6a6 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 (
{
- // 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/Link.tsx b/src/components/MDX/Link.tsx
index 7bf041e565a..ec5ba0ce306 100644
--- a/src/components/MDX/Link.tsx
+++ b/src/components/MDX/Link.tsx
@@ -17,7 +17,7 @@ function Link({
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') {
+ if (child.props?.['data-mdx-name'] === 'inlineCode') {
return cloneElement(child, {
isLink: true,
});
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index 9e99a4da692..e879eda2d54 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -381,7 +381,17 @@ 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 MDXComponents = annotateMDXComponents({
p: P,
strong: Strong,
blockquote: Blockquote,
@@ -450,11 +460,11 @@ 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;
- }
-}
+// 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 5e2da820e55..a4d5fa1405d 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 (
Date: Sun, 19 Jan 2025 17:44:59 +0100
Subject: [PATCH 08/39] fix code blocks
---
src/components/MDX/MDXComponents.tsx | 7 -------
src/components/MDX/Sandpack/createFileMap.ts | 15 ++++++++-------
2 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index e879eda2d54..7acc4934bbd 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -461,10 +461,3 @@ export const MDXComponents = annotateMDXComponents({
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/Sandpack/createFileMap.ts b/src/components/MDX/Sandpack/createFileMap.ts
index 7fbbe69feea..07bdcd377f4 100644
--- a/src/components/MDX/Sandpack/createFileMap.ts
+++ b/src/components/MDX/Sandpack/createFileMap.ts
@@ -13,20 +13,21 @@ 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}
+ 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
From bb73befcc5b651a0a5324064bdae88cfd6c6738e Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 18:02:30 +0100
Subject: [PATCH 09/39] fix max width
---
src/utils/compileMDX.tsx | 12 +++++-------
src/utils/prepareMDX.js | 9 +++++++--
2 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/src/utils/compileMDX.tsx b/src/utils/compileMDX.tsx
index a98a6c4fb5a..2490432ea0c 100644
--- a/src/utils/compileMDX.tsx
+++ b/src/utils/compileMDX.tsx
@@ -16,7 +16,7 @@ export default async function compileMDX(
mdx: string,
path: string | string[],
params: {[key: string]: any}
-): Promise<{Component: JSX.Element; toc: any; meta: any}> {
+): Promise<{content: JSX.Element; toc: any; meta: any}> {
// Cache setup
const store = new FileStore({
root: `${process.cwd()}/node_modules/.cache/react-docs-mdx/`,
@@ -70,21 +70,19 @@ export default async function compileMDX(
})
);
- // Parse frontmatter for metadata
const {data: meta} = grayMatter(mdx);
- // Run the compiled code with the runtime and get the default export
const {default: MDXContent} = await run(code, {
...runtime,
baseUrl: import.meta.url,
});
- // Prepare TOC (you can process toc within the MDX or separately)
- const {toc} = prepareMDX(MDXContent);
+ const {toc, children} = prepareMDX(
+
+ );
- // Return the ready-to-render React component
return {
- content: , // Replace {} with your custom components if needed
+ content: children,
toc,
meta,
};
diff --git a/src/utils/prepareMDX.js b/src/utils/prepareMDX.js
index 7277e6b5c29..e805605dbe3 100644
--- a/src/utils/prepareMDX.js
+++ b/src/utils/prepareMDX.js
@@ -34,8 +34,11 @@ function wrapChildrenInMaxWidthContainers(children) {
let finalChildren = [];
function flushWrapper(key) {
if (wrapQueue.length > 0) {
- const Wrapper = 'MaxWidth';
- finalChildren.push({wrapQueue} );
+ finalChildren.push(
+
+ {wrapQueue}
+
+ );
wrapQueue = [];
}
}
@@ -43,6 +46,7 @@ function wrapChildrenInMaxWidthContainers(children) {
if (child == null) {
return;
}
+
if (typeof child !== 'object') {
wrapQueue.push(child);
return;
@@ -54,6 +58,7 @@ function wrapChildrenInMaxWidthContainers(children) {
wrapQueue.push(child);
}
}
+
Children.forEach(children, handleChild);
flushWrapper('last');
return finalChildren;
From 5f0c4002ccd2111794b67275777b3755ff616e68 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 19:06:29 +0100
Subject: [PATCH 10/39] convert mdx post processing to actual plugins
---
package.json | 3 +-
src/utils/compileMDX.tsx | 194 ++++++++++++++++++++++++++++++++-------
src/utils/prepareMDX.js | 122 ------------------------
yarn.lock | 12 +++
4 files changed, 176 insertions(+), 155 deletions(-)
delete mode 100644 src/utils/prepareMDX.js
diff --git a/package.json b/package.json
index 8613226a387..3f57e473242 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,8 @@
"react-collapsed": "4.0.4",
"react-dom": "^19.0.0",
"remark-frontmatter": "^4.0.1",
- "remark-gfm": "^3.0.1"
+ "remark-gfm": "^3.0.1",
+ "unist-builder": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
diff --git a/src/utils/compileMDX.tsx b/src/utils/compileMDX.tsx
index 2490432ea0c..5a56c52085e 100644
--- a/src/utils/compileMDX.tsx
+++ b/src/utils/compileMDX.tsx
@@ -6,12 +6,140 @@ import * as runtime from 'react/jsx-runtime';
import {remarkPlugins} from '../../plugins/markdownToHtml';
import remarkGfm from 'remark-gfm';
import remarkFrontmatter from 'remark-frontmatter';
-import {prepareMDX} from './prepareMDX'; // Assuming prepareMDX is modularized
import {MDXComponents} from '../components/MDX/MDXComponents'; // Assuming MDXComponents is modularized
import visit from 'unist-util-visit';
+import {u} from 'unist-builder';
const DISK_CACHE_BREAKER = 11;
+export function remarkTOCExtractor({maxDepth = Infinity} = {}) {
+ return (tree, file) => {
+ const toc = [];
+
+ visit(tree, (node) => {
+ // Standard markdown headings
+ if (node.type === 'heading') {
+ if (node.depth > maxDepth) {
+ return;
+ }
+ const text = node.children
+ .filter((child) => child.type === 'text')
+ .map((child) => child.value)
+ .join('');
+ const id =
+ node.data?.hProperties?.id || text.toLowerCase().replace(/\s+/g, '-');
+
+ toc.push({
+ depth: node.depth,
+ text,
+ url: `#${id}`,
+ });
+ }
+
+ // MDX custom components (e.g., )
+ else if (node.type === 'mdxJsxFlowElement') {
+ switch (node.name) {
+ case 'TeamMember': {
+ // Extract attributes like name, permalink, etc.
+ let name = 'Team Member';
+ let permalink = 'team-member';
+
+ if (Array.isArray(node.attributes)) {
+ for (const attr of node.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,
+ text: name,
+ });
+ break;
+ }
+
+ // Similarly handle , , or any other custom tags if needed
+ case 'Challenges':
+ toc.push({
+ url: '#challenges',
+ depth: 2,
+ text: 'Challenges',
+ });
+ break;
+ case 'Recap':
+ toc.push({
+ url: '#recap',
+ depth: 2,
+ text: 'Recap',
+ });
+ break;
+ default:
+ break;
+ }
+ }
+ });
+
+ // Insert "Overview" at the top if there's at least one heading
+ if (toc.length > 0) {
+ toc.unshift({
+ url: '#',
+ text: 'Overview',
+ depth: 2,
+ });
+ }
+
+ file.data.toc = toc;
+ };
+}
+
+function remarkWrapElements() {
+ const fullWidthTypes = [
+ 'Sandpack',
+ 'FullWidth',
+ 'Illustration',
+ 'IllustrationBlock',
+ 'Challenges',
+ 'Recipes',
+ ];
+
+ return (tree) => {
+ const newChildren = [];
+ let wrapQueue = [];
+
+ function flushWrapper() {
+ if (wrapQueue.length > 0) {
+ newChildren.push(
+ u('mdxJsxFlowElement', {
+ name: 'MaxWidth',
+ attributes: [],
+ children: wrapQueue,
+ })
+ );
+ wrapQueue = [];
+ }
+ }
+
+ for (const node of tree.children) {
+ if (
+ node.type === 'mdxJsxFlowElement' &&
+ fullWidthTypes.includes(node.name)
+ ) {
+ flushWrapper();
+ newChildren.push(node);
+ } else {
+ wrapQueue.push(node);
+ }
+ }
+ flushWrapper();
+
+ tree.children = newChildren;
+ };
+}
+
export default async function compileMDX(
mdx: string,
path: string | string[],
@@ -44,46 +172,48 @@ export default async function compileMDX(
}
// Compile the MDX source code
- const code = String(
- await compile(mdx, {
- remarkPlugins: [...remarkPlugins, remarkGfm, remarkFrontmatter],
-
- 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;
- }
- });
- };
- },
- ],
- outputFormat: 'function-body',
- })
- );
+ const code = await compile(mdx, {
+ remarkPlugins: [
+ ...remarkPlugins,
+ remarkGfm,
+ remarkFrontmatter,
+ remarkTOCExtractor,
+ remarkWrapElements,
+ ],
+
+ 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;
+ }
+ });
+ };
+ },
+ ],
+ outputFormat: 'function-body',
+ });
const {data: meta} = grayMatter(mdx);
- const {default: MDXContent} = await run(code, {
+ const {default: MDXContent} = await run(String(code), {
...runtime,
baseUrl: import.meta.url,
});
- const {toc, children} = prepareMDX(
-
- );
+ const content = ;
return {
- content: children,
- toc,
+ content,
+ toc: code.data.toc,
meta,
};
}
diff --git a/src/utils/prepareMDX.js b/src/utils/prepareMDX.js
deleted file mode 100644
index e805605dbe3..00000000000
--- a/src/utils/prepareMDX.js
+++ /dev/null
@@ -1,122 +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 = 4;
-// !!! 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) {
- 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/yarn.lock b/yarn.lock
index 985ad4ef565..9d7fe6b67dd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1724,6 +1724,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"
@@ -8087,6 +8092,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 54ed2f7c5423de919580223b93eed192423707cd Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 19:10:08 +0100
Subject: [PATCH 11/39] fix tailwind
---
tailwind.config.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/tailwind.config.js b/tailwind.config.js
index f31a2451677..f709aba7d8f 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',
From da986d3cb3d62471ad245bed2bf569b1133ef547 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 19:20:42 +0100
Subject: [PATCH 12/39] fix incorrect iframe props
---
.../[[...markdownPath]]/{page.js => page.tsx} | 26 -------------------
src/content/versions.md | 2 +-
2 files changed, 1 insertion(+), 27 deletions(-)
rename src/app/[[...markdownPath]]/{page.js => page.tsx} (85%)
diff --git a/src/app/[[...markdownPath]]/page.js b/src/app/[[...markdownPath]]/page.tsx
similarity index 85%
rename from src/app/[[...markdownPath]]/page.js
rename to src/app/[[...markdownPath]]/page.tsx
index 701dbab191e..976f461a4fe 100644
--- a/src/app/[[...markdownPath]]/page.js
+++ b/src/app/[[...markdownPath]]/page.tsx
@@ -12,32 +12,6 @@ import {MDXComponents} from 'components/MDX/MDXComponents';
import compileMDX from 'utils/compileMDX';
import {generateRssFeed} from '../../utils/rss';
-// Deserialize a client React tree from JSON.
-function reviveNodeOnClient(parentPropertyName, val) {
- if (Array.isArray(val) && val[0] == '$r') {
- let Type = val[1];
- let key = val[2];
- if (key == null) {
- key = parentPropertyName;
- }
- 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;
- }
-}
-
function getActiveSection(pathname) {
if (pathname === '/') {
return 'home';
diff --git a/src/content/versions.md b/src/content/versions.md
index 8530f632476..8956e80e8d7 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
From 57e93aae0ac99c55ab9d77bdd303bb61f3fbb216 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 19:26:09 +0100
Subject: [PATCH 13/39] cleanup mdx dic
---
src/components/MDX/InlineToc.tsx | 10 +-
src/components/MDX/LanguageList.tsx | 39 +++++++
src/components/MDX/MDXComponents.tsx | 153 +--------------------------
src/components/MDX/Primitives.tsx | 23 ++++
4 files changed, 66 insertions(+), 159 deletions(-)
create mode 100644 src/components/MDX/LanguageList.tsx
create mode 100644 src/components/MDX/Primitives.tsx
diff --git a/src/components/MDX/InlineToc.tsx b/src/components/MDX/InlineToc.tsx
index eac0350262e..4f0186d7978 100644
--- a/src/components/MDX/InlineToc.tsx
+++ b/src/components/MDX/InlineToc.tsx
@@ -1,8 +1,9 @@
'use client';
import Link from 'next/link';
-import {HTMLAttributes, useContext, useMemo} from 'react';
+import {useContext, useMemo} from 'react';
import {Toc, TocContext, TocItem} from './TocContext';
+import {UL, LI} from './Primitives';
type NestedTocRoot = {
item: null;
@@ -44,13 +45,6 @@ export function InlineToc() {
return ;
}
-const LI = (p: HTMLAttributes) => (
-
-);
-const UL = (p: HTMLAttributes) => (
-
-);
-
function InlineTocItem({items}: {items: Array}) {
return (
diff --git a/src/components/MDX/LanguageList.tsx b/src/components/MDX/LanguageList.tsx
new file mode 100644
index 00000000000..66f59a49086
--- /dev/null
+++ b/src/components/MDX/LanguageList.tsx
@@ -0,0 +1,39 @@
+'use client';
+
+import Link from 'next/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/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx
index 7acc4934bbd..1c85b6d7577 100644
--- a/src/components/MDX/MDXComponents.tsx
+++ b/src/components/MDX/MDXComponents.tsx
@@ -31,15 +31,13 @@ import YouWillLearnCard from './YouWillLearnCard';
import {Challenges, Hint, Solution} from './Challenges';
import {IconNavArrow} from '../Icon/IconNavArrow';
import ButtonLink from 'components/ButtonLink';
-
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 (
@@ -63,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}
);
@@ -235,132 +212,6 @@ function Recipes(props: any) {
return ;
}
-// 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 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 (
diff --git a/src/components/MDX/Primitives.tsx b/src/components/MDX/Primitives.tsx
new file mode 100644
index 00000000000..cf04b367c7d
--- /dev/null
+++ b/src/components/MDX/Primitives.tsx
@@ -0,0 +1,23 @@
+import {HTMLAttributes} from 'react';
+
+export const P = (p: HTMLAttributes
) => (
+
+);
+
+export const Strong = (strong: HTMLAttributes) => (
+
+);
+
+export const OL = (p: HTMLAttributes) => (
+
+);
+export const LI = (p: HTMLAttributes) => (
+
+);
+export const UL = (p: HTMLAttributes) => (
+
+);
+
+export const Divider = () => (
+
+);
From c388c7a77320b210ebf9638c992f4e9454244b58 Mon Sep 17 00:00:00 2001
From: Jimmy Lai
Date: Sun, 19 Jan 2025 21:04:05 +0100
Subject: [PATCH 14/39] make it actually build
---
.eslintrc | 4 +-
package.json | 1 +
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/app/[[...markdownPath]]/page.tsx | 40 ++--
src/{_pages/500.js => app/error.tsx} | 6 +-
src/app/errors/[errorCode]/page.tsx | 90 +++++++
src/app/layout.tsx | 42 ++--
src/{_pages/404.js => app/not-found.tsx} | 11 +-
src/components/ErrorDecoderProvider.tsx | 19 ++
src/components/Layout/Page.tsx | 23 +-
src/hooks/usePendingRoute.ts | 34 +--
src/utils/compileMDX.tsx | 219 ------------------
src/utils/generateMDX.tsx | 127 ++++++++++
.../Seo.tsx => utils/generateMetadata.ts} | 15 +-
src/utils/mdx/MaxWidthWrapperPlugin.ts | 48 ++++
src/utils/mdx/MetaAttributesPlugin.ts | 19 ++
src/utils/mdx/TOCExtractorPlugin.ts | 120 ++++++++++
tsconfig.json | 15 +-
types.d.ts | 8 +
yarn.lock | 20 +-
24 files changed, 525 insertions(+), 887 deletions(-)
delete mode 100644 src/_pages/[[...markdownPath]].js
delete mode 100644 src/_pages/_app.tsx
delete mode 100644 src/_pages/_document.tsx
delete mode 100644 src/_pages/errors/[errorCode].tsx
delete mode 100644 src/_pages/errors/index.tsx
rename src/{_pages/500.js => app/error.tsx} (83%)
create mode 100644 src/app/errors/[errorCode]/page.tsx
rename src/{_pages/404.js => app/not-found.tsx} (74%)
create mode 100644 src/components/ErrorDecoderProvider.tsx
delete mode 100644 src/utils/compileMDX.tsx
create mode 100644 src/utils/generateMDX.tsx
rename src/{components/Seo.tsx => utils/generateMetadata.ts} (84%)
create mode 100644 src/utils/mdx/MaxWidthWrapperPlugin.ts
create mode 100644 src/utils/mdx/MetaAttributesPlugin.ts
create mode 100644 src/utils/mdx/TOCExtractorPlugin.ts
create mode 100644 types.d.ts
diff --git a/.eslintrc b/.eslintrc
index f8b03f98a19..2af52fb5361 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,7 +8,9 @@
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"react-hooks/exhaustive-deps": "error",
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
- "react-compiler/react-compiler": "error"
+ "react-compiler/react-compiler": "error",
+ "@next/next/no-img-element": "off",
+ "@next/next/no-html-link-for-pages": "off"
},
"env": {
"node": true,
diff --git a/package.json b/package.json
index 3f57e473242..b8a045dd063 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"@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",
diff --git a/src/_pages/[[...markdownPath]].js b/src/_pages/[[...markdownPath]].js
deleted file mode 100644
index bef4508df06..00000000000
--- 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 5431f87cc9e..00000000000
--- 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 6849df35d6a..00000000000
--- 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 127c5e00a26..00000000000
--- 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/MDXComponentsWrapper';
-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 d7742f03a30..00000000000
--- 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/app/[[...markdownPath]]/page.tsx b/src/app/[[...markdownPath]]/page.tsx
index 976f461a4fe..0012ed598d8 100644
--- a/src/app/[[...markdownPath]]/page.tsx
+++ b/src/app/[[...markdownPath]]/page.tsx
@@ -1,5 +1,3 @@
-// src/app/[[...markdownPath]]/page.js
-import {Fragment} from 'react';
import fs from 'fs/promises';
import path from 'path';
import {Page} from 'components/Layout/Page';
@@ -8,11 +6,11 @@ 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 {generateMDX} from '../../utils/generateMDX';
import {generateRssFeed} from '../../utils/rss';
+import {RouteItem} from 'components/Layout/getRouteMeta';
-function getActiveSection(pathname) {
+function getActiveSection(pathname: string) {
if (pathname === '/') {
return 'home';
} else if (pathname.startsWith('/reference')) {
@@ -28,7 +26,7 @@ function getActiveSection(pathname) {
}
}
-async function getRouteTree(section) {
+async function getRouteTree(section: string) {
switch (section) {
case 'home':
case 'unknown':
@@ -41,11 +39,13 @@ async function getRouteTree(section) {
return sidebarCommunity;
case 'blog':
return sidebarBlog;
+ default:
+ throw new Error(`Unknown section: ${section}`);
}
}
// This replaces getStaticProps
-async function getPageContent(markdownPath) {
+async function getPageContent(markdownPath: any[]) {
const rootDir = path.join(process.cwd(), 'src/content');
let mdxPath = markdownPath?.join('/') || 'index';
let mdx;
@@ -61,14 +61,14 @@ async function getPageContent(markdownPath) {
await generateRssFeed();
}
- return await compileMDX(mdx, mdxPath, {});
+ return await generateMDX(mdx, mdxPath, {});
}
// This replaces getStaticPaths
export async function generateStaticParams() {
const rootDir = path.join(process.cwd(), 'src/content');
- async function getFiles(dir) {
+ async function getFiles(dir: string): Promise {
const entries = await fs.readdir(dir, {withFileTypes: true});
const files = await Promise.all(
entries.map(async (entry) => {
@@ -78,12 +78,15 @@ export async function generateStaticParams() {
: res.slice(rootDir.length + 1);
})
);
+
return files
.flat()
- .filter((file) => file.endsWith('.md') && !file.startsWith('errors/'));
+ .filter(
+ (file: string) => file.endsWith('.md') && !file.startsWith('errors/')
+ );
}
- function getSegments(file) {
+ function getSegments(file: string) {
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
if (segments[segments.length - 1] === 'index') {
segments.pop();
@@ -93,15 +96,20 @@ export async function generateStaticParams() {
const files = await getFiles(rootDir);
- return files.map((file) => ({
+ return files.map((file: any) => ({
markdownPath: getSegments(file),
}));
}
-export default async function WrapperPage({params}) {
+
+export default async function WrapperPage({
+ params,
+}: {
+ params: Promise<{markdownPath: any[]}>;
+}) {
const {markdownPath} = await params;
// Get the MDX content and associated data
- const {content, toc, meta, languages} = await getPageContent(markdownPath);
+ const {content, toc, meta} = await getPageContent(markdownPath);
const pathname = '/' + (markdownPath?.join('/') || '');
const section = getActiveSection(pathname);
@@ -111,10 +119,10 @@ export default async function WrapperPage({params}) {
return (
+ languages={null}>
{content}
);
diff --git a/src/_pages/500.js b/src/app/error.tsx
similarity index 83%
rename from src/_pages/500.js
rename to src/app/error.tsx
index b043e35b262..195152b46ab 100644
--- a/src/_pages/500.js
+++ b/src/app/error.tsx
@@ -1,3 +1,5 @@
+'use client';
+
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
@@ -5,14 +7,16 @@
import {Page} from 'components/Layout/Page';
import {MDXComponents} from 'components/MDX/MDXComponents';
import sidebarLearn from '../sidebarLearn.json';
+import {RouteItem} from 'components/Layout/getRouteMeta';
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
export default function NotFound() {
return (
diff --git a/src/app/errors/[errorCode]/page.tsx b/src/app/errors/[errorCode]/page.tsx
new file mode 100644
index 00000000000..0609f948057
--- /dev/null
+++ b/src/app/errors/[errorCode]/page.tsx
@@ -0,0 +1,90 @@
+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';
+
+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();
+
+ return Object.keys(errorCodes!).map((code) => ({
+ errorCode: code,
+ }));
+}
+
+async function getErrorPageContent(params: {errorCode?: string}) {
+ const errorCodes = await getErrorCodes();
+
+ const code = params?.errorCode;
+
+ 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: errorCodes[code],
+ };
+}
+
+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;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2344e71d3ab..ad77d7111b9 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,16 +1,17 @@
-// app/layout.jsx
import {siteConfig} from '../siteConfig';
import Script from 'next/script';
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 {generateMetadata as generateSeoMetadata} from 'components/Seo';
+import {generateMetadata as generateSeoMetadata} from '../utils/generateMetadata';
+import {Suspense} from 'react';
-export async function generateMetadata({params}) {
+export async function generateMetadata() {
const metadata = generateSeoMetadata({
title: 'React',
isHomePage: true,
@@ -18,30 +19,17 @@ export async function generateMetadata({params}) {
});
return {
- ...metadata,
- 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',
- themeColor: [
- {media: '(prefers-color-scheme: light)', color: '#23272f'},
- {media: '(prefers-color-scheme: dark)', color: '#23272f'},
- ],
- other: {
- 'msapplication-TileColor': '#2b5797',
- },
+ metadata,
};
}
+export const viewport = {
+ themeColor: [
+ {media: '(prefers-color-scheme: light)', color: '#23272f'},
+ {media: '(prefers-color-scheme: dark)', color: '#23272f'},
+ ],
+};
+
function ThemeScript() {
return (