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 ( + + + + + + + + + + + + ); +} 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 ( -
-
- {alt} - {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 ( +//
+//
+// {alt} +// {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.alt} -
- {info.caption ? ( -
- {info.caption} -
- ) : null} -
- )); - return ( - -
- {sequential ? ( -
    - {images.map((x: any, i: number) => ( -
  1. - {x} -
  2. - ))} -
- ) : ( -
{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.alt} +//
+// {info.caption ? ( +//
+// {info.caption} +//
+// ) : null} +//
+// )); +// return ( +// +//
+// {sequential ? ( +//
    +// {images.map((x: any, i: number) => ( +//
  1. +// {x} +//
  2. +// ))} +//
+// ) : ( +//
{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 ( +
+
+ {alt} + {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.alt} +
+ {info.caption ? ( +
+ {info.caption} +
+ ) : null} +
+ )); + return ( + +
+ {sequential ? ( +
    + {images.map((x: any, i: number) => ( +
  1. + {x} +
  2. + ))} +
+ ) : ( +
{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 {alt}; } -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: - + 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) => ( -
          1. -); -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 ( -//
              -//
              -// {alt} -// {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.alt} -//
              -// {info.caption ? ( -//
              -// {info.caption} -//
              -// ) : null} -//
              -// )); -// return ( -// -//
              -// {sequential ? ( -//
                -// {images.map((x: any, i: number) => ( -//
              1. -// {x} -//
              2. -// ))} -//
              -// ) : ( -//
              {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) => ( +
              1. +); +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 ( - - - - - - - - - -