mirror of
https://github.com/reactjs/react.dev.git
synced 2026-02-23 12:13:11 +00:00
This reverts commit 0eb0f889b1.
This commit is contained in:
@@ -8,9 +8,7 @@
|
||||
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
|
||||
"react-compiler/react-compiler": "error",
|
||||
"@next/next/no-img-element": "off",
|
||||
"@next/next/no-html-link-for-pages": "off"
|
||||
"react-compiler/react-compiler": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -11,11 +11,8 @@ const nextConfig = {
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
reactCompiler: true,
|
||||
newDevOverlay: true,
|
||||
},
|
||||
|
||||
env: {},
|
||||
serverExternalPackages: [],
|
||||
webpack: (config, {dev, isServer, ...options}) => {
|
||||
if (process.env.ANALYZE) {
|
||||
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
|
||||
|
||||
14
package.json
14
package.json
@@ -5,7 +5,7 @@
|
||||
"license": "CC",
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"dev": "next dev",
|
||||
"dev": "next-remote-watch ./src/content",
|
||||
"build": "next build && node --experimental-modules ./scripts/downloadFonts.mjs",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
@@ -15,11 +15,12 @@
|
||||
"prettier:diff": "yarn nit:source",
|
||||
"lint-heading-ids": "node scripts/headingIdLinter.js",
|
||||
"fix-headings": "node scripts/headingIdLinter.js --fix",
|
||||
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids",
|
||||
"ci-check": "npm-run-all prettier:diff --parallel lint tsc lint-heading-ids rss",
|
||||
"tsc": "tsc --noEmit",
|
||||
"start": "next start",
|
||||
"postinstall": "is-ci || husky install .husky",
|
||||
"check-all": "npm-run-all prettier lint:fix tsc"
|
||||
"check-all": "npm-run-all prettier lint:fix tsc rss",
|
||||
"rss": "node scripts/generateRss.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codesandbox/sandpack-react": "2.13.5",
|
||||
@@ -27,21 +28,19 @@
|
||||
"@docsearch/react": "^3.8.3",
|
||||
"@headlessui/react": "^1.7.0",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"body-scroll-lock": "^3.1.3",
|
||||
"classnames": "^2.2.6",
|
||||
"date-fns": "^2.16.1",
|
||||
"debounce": "^1.2.1",
|
||||
"github-slugger": "^1.3.0",
|
||||
"next": "^15.2.0-canary.33",
|
||||
"next": "15.1.0",
|
||||
"next-remote-watch": "^1.0.0",
|
||||
"parse-numeric-range": "^1.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-collapsed": "4.0.4",
|
||||
"react-dom": "^19.0.0",
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"unist-builder": "^4.0.0"
|
||||
"remark-gfm": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
@@ -63,7 +62,6 @@
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-eslint": "10.x",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-e552027-20250112",
|
||||
"chokidar": "^4.0.3",
|
||||
"eslint": "7.x",
|
||||
"eslint-config-next": "12.0.3",
|
||||
"eslint-config-react-app": "^5.2.1",
|
||||
|
||||
6
scripts/generateRss.js
Normal file
6
scripts/generateRss.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
const {generateRssFeed} = require('../src/utils/rss');
|
||||
|
||||
generateRssFeed();
|
||||
@@ -1,172 +0,0 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import sidebarHome from '../../sidebarHome.json';
|
||||
import sidebarLearn from '../../sidebarLearn.json';
|
||||
import sidebarReference from '../../sidebarReference.json';
|
||||
import sidebarCommunity from '../../sidebarCommunity.json';
|
||||
import sidebarBlog from '../../sidebarBlog.json';
|
||||
import {generateMDX} from '../../utils/generateMDX';
|
||||
|
||||
import {generateMetadata as generateSeoMetadata} from '../../utils/generateMetadata';
|
||||
|
||||
import {getRouteMeta, RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {LanguageItem} from 'components/MDX/LanguagesContext';
|
||||
import {cache} from 'react';
|
||||
|
||||
function getActiveSection(pathname: string) {
|
||||
if (pathname === '/') {
|
||||
return 'home';
|
||||
} else if (pathname.startsWith('/reference')) {
|
||||
return 'reference';
|
||||
} else if (pathname.startsWith('/learn')) {
|
||||
return 'learn';
|
||||
} else if (pathname.startsWith('/community')) {
|
||||
return 'community';
|
||||
} else if (pathname.startsWith('/blog')) {
|
||||
return 'blog';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
async function getRouteTree(section: string) {
|
||||
switch (section) {
|
||||
case 'home':
|
||||
case 'unknown':
|
||||
return sidebarHome;
|
||||
case 'learn':
|
||||
return sidebarLearn;
|
||||
case 'reference':
|
||||
return sidebarReference;
|
||||
case 'community':
|
||||
return sidebarCommunity;
|
||||
case 'blog':
|
||||
return sidebarBlog;
|
||||
default:
|
||||
throw new Error(`Unknown section: ${section}`);
|
||||
}
|
||||
}
|
||||
|
||||
const getPageContent = cache(async function getPageContent(
|
||||
markdownPath: any[]
|
||||
) {
|
||||
const rootDir = path.join(process.cwd(), 'src/content');
|
||||
let mdxPath = markdownPath?.join('/') || 'index';
|
||||
let mdx;
|
||||
|
||||
try {
|
||||
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
|
||||
} catch {
|
||||
mdx = await fs.readFile(path.join(rootDir, mdxPath, 'index.md'), 'utf8');
|
||||
}
|
||||
|
||||
return await generateMDX(mdx, mdxPath, {});
|
||||
});
|
||||
|
||||
// This replaces getStaticPaths
|
||||
export async function generateStaticParams() {
|
||||
const rootDir = path.join(process.cwd(), 'src/content');
|
||||
|
||||
async function getFiles(dir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, {withFileTypes: true});
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const res = path.resolve(dir, entry.name);
|
||||
return entry.isDirectory()
|
||||
? getFiles(res)
|
||||
: res.slice(rootDir.length + 1);
|
||||
})
|
||||
);
|
||||
|
||||
return files
|
||||
.flat()
|
||||
.filter(
|
||||
(file: string) => file.endsWith('.md') && !file.startsWith('errors/')
|
||||
);
|
||||
}
|
||||
|
||||
function getSegments(file: string) {
|
||||
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
|
||||
if (segments[segments.length - 1] === 'index') {
|
||||
segments.pop();
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
const files = await getFiles(rootDir);
|
||||
|
||||
return files.map((file: any) => ({
|
||||
markdownPath: getSegments(file),
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function WrapperPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{markdownPath: string[]}>;
|
||||
}) {
|
||||
const {markdownPath} = await params;
|
||||
|
||||
// Get the MDX content and associated data
|
||||
const {content, toc, meta} = await getPageContent(markdownPath);
|
||||
|
||||
const pathname = '/' + (markdownPath?.join('/') || '');
|
||||
const section = getActiveSection(pathname);
|
||||
const routeTree = await getRouteTree(section);
|
||||
|
||||
// Load the list of translated languages conditionally.
|
||||
let languages: Array<LanguageItem> | null = null;
|
||||
if (pathname.endsWith('/translations')) {
|
||||
languages = await (
|
||||
await fetch(
|
||||
'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'
|
||||
)
|
||||
).json(); // { code: string; name: string; enName: string}[]
|
||||
}
|
||||
|
||||
// Pass the content and TOC directly, as `getPageContent` should already return them in the correct format
|
||||
return (
|
||||
<Page
|
||||
toc={toc} // Pass the TOC directly without parsing
|
||||
routeTree={routeTree as RouteItem}
|
||||
meta={meta}
|
||||
section={section}
|
||||
pathname={pathname}
|
||||
languages={languages}>
|
||||
{content}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
// Configure dynamic segments to be statically generated
|
||||
export const dynamicParams = false;
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{markdownPath: string[]}>;
|
||||
}) {
|
||||
const {markdownPath} = await params;
|
||||
const pathname = '/' + (markdownPath?.join('/') || '');
|
||||
const section = getActiveSection(pathname);
|
||||
const routeTree = await getRouteTree(section);
|
||||
const {route, order} = getRouteMeta(pathname, routeTree as RouteItem);
|
||||
const {
|
||||
title = route?.title || '',
|
||||
description = route?.description || '',
|
||||
titleForTitleTag,
|
||||
} = await getPageContent(markdownPath).then(({meta}) => meta);
|
||||
|
||||
return generateSeoMetadata({
|
||||
title,
|
||||
isHomePage: pathname === '/',
|
||||
path: pathname,
|
||||
description,
|
||||
titleForTitleTag,
|
||||
image: `/images/og-${section}.png`,
|
||||
searchOrder:
|
||||
section === 'learn' || (section === 'blog' && pathname !== '/blog')
|
||||
? order
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import sidebarLearn from '../../../sidebarLearn.json';
|
||||
import type {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {generateMDX} from 'utils/generateMDX';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {ErrorDecoderProvider} from 'components/ErrorDecoderProvider';
|
||||
import {notFound} from 'next/navigation';
|
||||
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
|
||||
|
||||
let errorCodesCache: Record<string, string> | null = null;
|
||||
|
||||
async function getErrorCodes() {
|
||||
if (!errorCodesCache) {
|
||||
const response = await fetch(
|
||||
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
|
||||
);
|
||||
errorCodesCache = await response.json();
|
||||
}
|
||||
return errorCodesCache;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const errorCodes = await getErrorCodes();
|
||||
|
||||
const staticParams = Object.keys(errorCodes!).map((code) => ({
|
||||
errorCode: [code],
|
||||
})) as Array<{errorCode: string[] | undefined}>;
|
||||
|
||||
staticParams.push({errorCode: undefined});
|
||||
|
||||
return staticParams;
|
||||
}
|
||||
|
||||
async function getErrorPageContent(params: {errorCode: string[]}) {
|
||||
if (params.errorCode?.length > 1) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const code = params.errorCode?.[0];
|
||||
|
||||
const errorCodes = await getErrorCodes();
|
||||
|
||||
if (code && !errorCodes?.[code]) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const rootDir = path.join(process.cwd(), 'src/content/errors');
|
||||
let mdxPath = params?.errorCode || 'index';
|
||||
let mdx;
|
||||
|
||||
try {
|
||||
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
|
||||
} catch {
|
||||
mdx = await fs.readFile(path.join(rootDir, 'generic.md'), 'utf8');
|
||||
}
|
||||
|
||||
const {content, toc, meta} = await generateMDX(mdx, mdxPath, {
|
||||
code,
|
||||
errorCodes,
|
||||
});
|
||||
|
||||
return {
|
||||
content,
|
||||
toc,
|
||||
meta,
|
||||
errorCode: code,
|
||||
errorMessage: code ? errorCodes![code] : null,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ErrorDecoderPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{errorCode: string[]}>;
|
||||
}) {
|
||||
const {content, errorMessage, errorCode} = await getErrorPageContent(
|
||||
await params
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorDecoderProvider errorMessage={errorMessage} errorCode={errorCode}>
|
||||
<Page
|
||||
pathname={`/errors/${errorCode}`}
|
||||
toc={[]}
|
||||
meta={{
|
||||
title: errorCode
|
||||
? `Minified React error #${errorCode}`
|
||||
: 'Minified Error Decoder',
|
||||
}}
|
||||
routeTree={sidebarLearn as RouteItem}
|
||||
section="unknown">
|
||||
<div className="whitespace-pre-line">{content}</div>
|
||||
</Page>
|
||||
</ErrorDecoderProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Disable dynamic params to ensure all pages are statically generated
|
||||
export const dynamicParams = false;
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{errorCode: string[]}>;
|
||||
}) {
|
||||
const {errorCode} = await params;
|
||||
|
||||
const title = errorCode
|
||||
? `Minified React error #${errorCode}`
|
||||
: 'Minified Error Decoder';
|
||||
|
||||
return generateSeoMetadata({
|
||||
title,
|
||||
path: `errors/${errorCode}`,
|
||||
isHomePage: false,
|
||||
});
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import {siteConfig} from '../siteConfig';
|
||||
import {Analytics} from 'components/Analytics';
|
||||
import {ScrollHandler} from 'components/SafariScrollHandler';
|
||||
|
||||
import '@docsearch/css';
|
||||
import '../styles/algolia.css';
|
||||
import '../styles/index.css';
|
||||
import '../styles/sandpack.css';
|
||||
|
||||
import {Suspense} from 'react';
|
||||
import {DevContentRefresher} from 'components/DevContentRefresher';
|
||||
import {ThemeScript} from 'components/ThemeScript';
|
||||
import {UwuScript} from 'components/UwuScript';
|
||||
import {Metadata} from 'next';
|
||||
|
||||
export const viewport = {
|
||||
themeColor: [
|
||||
{media: '(prefers-color-scheme: light)', color: '#23272f'},
|
||||
{media: '(prefers-color-scheme: dark)', color: '#23272f'},
|
||||
],
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript.',
|
||||
openGraph: {
|
||||
siteName: 'React',
|
||||
type: 'website',
|
||||
images: [{url: '/images/og-default.png'}],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
site: '@reactjs',
|
||||
creator: '@reactjs',
|
||||
images: ['/images/og-default.png'],
|
||||
},
|
||||
verification: {
|
||||
google: 'sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0',
|
||||
},
|
||||
other: {
|
||||
'msapplication-TileColor': '#2b5797',
|
||||
'fb:app_id': '623268441017527',
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png'},
|
||||
{url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png'},
|
||||
],
|
||||
apple: [
|
||||
{url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png'},
|
||||
],
|
||||
other: [
|
||||
{rel: 'mask-icon', url: '/safari-pinned-tab.svg', color: '#404756'},
|
||||
],
|
||||
},
|
||||
manifest: '/site.webmanifest',
|
||||
};
|
||||
|
||||
function FontPreload() {
|
||||
return (
|
||||
<>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Source-Code-Pro-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_Md.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_SBd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_MdIt.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Text_W_BdIt.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Display_W_Bd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Text_W_Md.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Text_W_Bd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Text_W_Rg.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Optimistic_Text_W_It.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<html
|
||||
lang={siteConfig.languageCode}
|
||||
dir={siteConfig.isRTL ? 'rtl' : 'ltr'}
|
||||
suppressHydrationWarning>
|
||||
<head>
|
||||
<FontPreload />
|
||||
</head>
|
||||
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
|
||||
<ScrollHandler />
|
||||
<ThemeScript />
|
||||
<UwuScript />
|
||||
{process.env.NODE_ENV !== 'production' && <DevContentRefresher />}
|
||||
{children}
|
||||
<Suspense fallback={null}>
|
||||
<Analytics />
|
||||
</Suspense>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import Feed from 'rss';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
const getAllFiles = (dirPath, arrayOfFiles = []) => {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(dirPath, file);
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
|
||||
} else {
|
||||
arrayOfFiles.push(filePath);
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
};
|
||||
|
||||
export async function GET() {
|
||||
const feed = new Feed({
|
||||
title: 'React Blog',
|
||||
description:
|
||||
'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.',
|
||||
feed_url: 'https://react.dev/rss.xml',
|
||||
site_url: 'https://react.dev/',
|
||||
language: 'en',
|
||||
favicon: 'https://react.dev/favicon.ico',
|
||||
pubDate: new Date(),
|
||||
generator: 'react.dev rss module',
|
||||
});
|
||||
|
||||
const dirPath = path.join(process.cwd(), 'src/content/blog');
|
||||
const filesByOldest = getAllFiles(dirPath);
|
||||
const files = filesByOldest.reverse();
|
||||
|
||||
for (const filePath of files) {
|
||||
const id = path.basename(filePath);
|
||||
if (id !== 'index.md') {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const {data} = matter(content);
|
||||
const slug = filePath.split('/').slice(-4).join('/').replace('.md', '');
|
||||
|
||||
if (!data.title || !data.author || !data.date || !data.description) {
|
||||
throw new Error(
|
||||
`${id}: Blog posts must include title, author, date, and description in metadata.`
|
||||
);
|
||||
}
|
||||
|
||||
feed.item({
|
||||
id,
|
||||
title: data.title,
|
||||
author: data.author,
|
||||
date: new Date(data.date),
|
||||
url: `https://react.dev/blog/${slug}`,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(feed.xml({indent: true}), {
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import Script from 'next/script';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gtag: (...args: any[]) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export function Analytics() {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
|
||||
const handleTermination = () => {
|
||||
window.gtag?.('event', 'timing', {
|
||||
event_label: 'JS Dependencies',
|
||||
event: 'unload',
|
||||
});
|
||||
};
|
||||
window.addEventListener(terminationEvent, handleTermination);
|
||||
return () =>
|
||||
window.removeEventListener(terminationEvent, handleTermination);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// If only we had router events. But patching pushState is what Vercel Analytics does.
|
||||
// https://va.vercel-scripts.com/v1/script.debug.js
|
||||
const originalPushState = history.pushState;
|
||||
|
||||
history.pushState = function (...args) {
|
||||
const oldCleanedUrl = window.location.href.split(/[\?\#]/)[0];
|
||||
originalPushState.apply(history, args);
|
||||
const newCleanedUrl = window.location.href.split(/[\?\#]/)[0];
|
||||
if (oldCleanedUrl !== newCleanedUrl) {
|
||||
window?.gtag('set', 'page', newCleanedUrl);
|
||||
window?.gtag('send', 'pageview');
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
history.pushState = originalPushState;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}`}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${process.env.NEXT_PUBLIC_GA_TRACKING_ID}');
|
||||
`}
|
||||
</Script>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
export function DevContentRefresher() {
|
||||
const router = useRouter();
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
wsRef.current = new WebSocket('ws://localhost:3001');
|
||||
|
||||
wsRef.current.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.event === 'refresh') {
|
||||
console.log('Refreshing content...');
|
||||
// @ts-ignore
|
||||
router.hmrRefresh(); // Triggers client-side refresh
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
wsRef.current?.close();
|
||||
};
|
||||
}, [router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {ErrorDecoderContext} from './ErrorDecoderContext';
|
||||
|
||||
export function ErrorDecoderProvider({
|
||||
children,
|
||||
errorMessage,
|
||||
errorCode,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
errorMessage: string | null;
|
||||
errorCode: string | null;
|
||||
}) {
|
||||
return (
|
||||
<ErrorDecoderContext.Provider value={{errorMessage, errorCode}}>
|
||||
{children}
|
||||
</ErrorDecoderContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
|
||||
import {useState} from 'react';
|
||||
import {useRouter} from 'next/router';
|
||||
import cn from 'classnames';
|
||||
import {usePathname} from 'next/navigation';
|
||||
|
||||
export function Feedback({onSubmit = () => {}}: {onSubmit?: () => void}) {
|
||||
const pathname = usePathname();
|
||||
const cleanedPath = pathname.split(/[\?\#]/)[0];
|
||||
const {asPath} = useRouter();
|
||||
const cleanedPath = asPath.split(/[\?\#]/)[0];
|
||||
// Reset on route changes.
|
||||
return <SendFeedback key={cleanedPath} onSubmit={onSubmit} />;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Suspense} from 'react';
|
||||
import * as React from 'react';
|
||||
import {useRouter} from 'next/router';
|
||||
import {SidebarNav} from './SidebarNav';
|
||||
import {Footer} from './Footer';
|
||||
import {Toc} from './Toc';
|
||||
// import SocialBanner from '../SocialBanner';
|
||||
import {DocsPageFooter} from 'components/DocsFooter';
|
||||
|
||||
import {Seo} from 'components/Seo';
|
||||
import PageHeading from 'components/PageHeading';
|
||||
import {getRouteMeta} from './getRouteMeta';
|
||||
import {TocContext} from '../MDX/TocContext';
|
||||
@@ -20,8 +20,8 @@ import type {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {HomeContent} from './HomeContent';
|
||||
import {TopNav} from './TopNav';
|
||||
import cn from 'classnames';
|
||||
import Head from 'next/head';
|
||||
|
||||
// Prefetch the code block component
|
||||
import(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock');
|
||||
|
||||
interface PageProps {
|
||||
@@ -36,7 +36,6 @@ interface PageProps {
|
||||
};
|
||||
section: 'learn' | 'reference' | 'community' | 'blog' | 'home' | 'unknown';
|
||||
languages?: Languages | null;
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
export function Page({
|
||||
@@ -45,11 +44,11 @@ export function Page({
|
||||
routeTree,
|
||||
meta,
|
||||
section,
|
||||
pathname,
|
||||
languages = null,
|
||||
}: PageProps) {
|
||||
const cleanedPath = pathname.split(/[\?\#]/)[0];
|
||||
const {route, nextRoute, prevRoute, breadcrumbs} = getRouteMeta(
|
||||
const {asPath} = useRouter();
|
||||
const cleanedPath = asPath.split(/[\?\#]/)[0];
|
||||
const {route, nextRoute, prevRoute, breadcrumbs, order} = getRouteMeta(
|
||||
cleanedPath,
|
||||
routeTree
|
||||
);
|
||||
@@ -114,17 +113,31 @@ export function Page({
|
||||
showSidebar = false;
|
||||
}
|
||||
|
||||
let searchOrder;
|
||||
if (section === 'learn' || (section === 'blog' && !isBlogIndex)) {
|
||||
searchOrder = order;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Seo
|
||||
title={title}
|
||||
titleForTitleTag={meta.titleForTitleTag}
|
||||
isHomePage={isHomePage}
|
||||
image={`/images/og-` + section + '.png'}
|
||||
searchOrder={searchOrder}
|
||||
/>
|
||||
{(isHomePage || isBlogIndex) && (
|
||||
// RSS Feed link is now handled by metadata in layout.tsx
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="React Blog RSS Feed"
|
||||
href="/rss.xml"
|
||||
/>
|
||||
<Head>
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title="React Blog RSS Feed"
|
||||
href="/rss.xml"
|
||||
/>
|
||||
</Head>
|
||||
)}
|
||||
{/*<SocialBanner />*/}
|
||||
<TopNav
|
||||
section={section}
|
||||
routeTree={routeTree}
|
||||
@@ -149,7 +162,9 @@ export function Page({
|
||||
{/* No fallback UI so need to be careful not to suspend directly inside. */}
|
||||
<Suspense fallback={null}>
|
||||
<main className="min-w-0 isolate">
|
||||
<article className="font-normal break-words text-primary dark:text-primary-dark">
|
||||
<article
|
||||
className="font-normal break-words text-primary dark:text-primary-dark"
|
||||
key={asPath}>
|
||||
{content}
|
||||
</article>
|
||||
<div
|
||||
@@ -173,7 +188,7 @@ export function Page({
|
||||
</main>
|
||||
</Suspense>
|
||||
<div className="hidden -mt-16 lg:max-w-custom-xs 2xl:block">
|
||||
{showToc && toc.length > 0 && <Toc headings={toc} key={pathname} />}
|
||||
{showToc && toc.length > 0 && <Toc headings={toc} key={asPath} />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
import {useRef, useLayoutEffect, Fragment} from 'react';
|
||||
|
||||
import cn from 'classnames';
|
||||
import {useRouter} from 'next/router';
|
||||
import {SidebarLink} from './SidebarLink';
|
||||
import {useCollapse} from 'react-collapsed';
|
||||
import usePendingRoute from 'hooks/usePendingRoute';
|
||||
import type {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {siteConfig} from 'siteConfig';
|
||||
import {usePathname} from 'next/navigation';
|
||||
|
||||
interface SidebarRouteTreeProps {
|
||||
isForceExpanded: boolean;
|
||||
@@ -77,7 +77,7 @@ export function SidebarRouteTree({
|
||||
routeTree,
|
||||
level = 0,
|
||||
}: SidebarRouteTreeProps) {
|
||||
const slug = usePathname().split(/[\?\#]/)[0];
|
||||
const slug = useRouter().asPath.split(/[\?\#]/)[0];
|
||||
const pendingRoute = usePendingRoute();
|
||||
const currentRoutes = routeTree.routes as RouteItem[];
|
||||
return (
|
||||
|
||||
@@ -11,11 +11,7 @@ export function Toc({headings}: {headings: Toc}) {
|
||||
// TODO: We currently have a mismatch between the headings in the document
|
||||
// and the headings we find in MarkdownPage (i.e. we don't find Recap or Challenges).
|
||||
// Select the max TOC item we have here for now, but remove this after the fix.
|
||||
const selectedIndex =
|
||||
currentIndex !== undefined
|
||||
? Math.min(currentIndex, headings.length - 1)
|
||||
: -1;
|
||||
|
||||
const selectedIndex = Math.min(currentIndex, headings.length - 1);
|
||||
return (
|
||||
<nav role="navigation" className="pt-20 sticky top-0 end-0">
|
||||
{headings.length > 0 && (
|
||||
@@ -55,7 +51,7 @@ export function Toc({headings}: {headings: Toc}) {
|
||||
'block hover:text-link dark:hover:text-link-dark leading-normal py-2'
|
||||
)}
|
||||
href={h.url}>
|
||||
{h.node}
|
||||
{h.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import Image from 'next/image';
|
||||
import * as React from 'react';
|
||||
import cn from 'classnames';
|
||||
import NextLink from 'next/link';
|
||||
import {useRouter} from 'next/router';
|
||||
import {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock';
|
||||
|
||||
import {IconClose} from 'components/Icon/IconClose';
|
||||
@@ -26,7 +27,6 @@ import {SidebarRouteTree} from '../Sidebar';
|
||||
import type {RouteItem} from '../getRouteMeta';
|
||||
import {siteConfig} from 'siteConfig';
|
||||
import BrandMenu from './BrandMenu';
|
||||
import {usePathname} from 'next/navigation';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -162,7 +162,7 @@ export default function TopNav({
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const scrollParentRef = useRef<HTMLDivElement>(null);
|
||||
const pathname = usePathname();
|
||||
const {asPath} = useRouter();
|
||||
|
||||
// HACK. Fix up the data structures instead.
|
||||
if ((routeTree as any).routes.length === 1) {
|
||||
@@ -183,7 +183,7 @@ export default function TopNav({
|
||||
// Close the overlay on any navigation.
|
||||
useEffect(() => {
|
||||
setIsMenuOpen(false);
|
||||
}, [pathname]);
|
||||
}, [asPath]);
|
||||
|
||||
// Also close the overlay if the window gets resized past mobile layout.
|
||||
// (This is also important because we don't want to keep the body locked!)
|
||||
|
||||
@@ -23,10 +23,7 @@ export function getHeaderAnchors(): HTMLAnchorElement[] {
|
||||
* Sets up Table of Contents highlighting.
|
||||
*/
|
||||
export function useTocHighlight() {
|
||||
const [currentIndex, setCurrentIndex] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
@@ -11,7 +9,7 @@ import {H2} from 'components/MDX/Heading';
|
||||
import {H4} from 'components/MDX/Heading';
|
||||
import {Challenge} from './Challenge';
|
||||
import {Navigation} from './Navigation';
|
||||
import {usePathname} from 'next/navigation';
|
||||
import {useRouter} from 'next/router';
|
||||
|
||||
interface ChallengesProps {
|
||||
children: React.ReactElement[];
|
||||
@@ -42,13 +40,11 @@ const parseChallengeContents = (
|
||||
let challenge: Partial<ChallengeContents> = {};
|
||||
let content: React.ReactElement[] = [];
|
||||
Children.forEach(children, (child) => {
|
||||
const {props} = child as React.ReactElement<{
|
||||
const {props, type} = child as React.ReactElement<{
|
||||
children?: string;
|
||||
id?: string;
|
||||
'data-mdx-name'?: string;
|
||||
}>;
|
||||
|
||||
switch (props?.['data-mdx-name']) {
|
||||
switch ((type as any).mdxName) {
|
||||
case 'Solution': {
|
||||
challenge.solution = child;
|
||||
challenge.content = content;
|
||||
@@ -94,12 +90,12 @@ export function Challenges({
|
||||
const queuedScrollRef = useRef<undefined | QueuedScroll>(QueuedScroll.INIT);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const currentChallenge = challenges[activeIndex];
|
||||
const pathname = usePathname();
|
||||
const {asPath} = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (queuedScrollRef.current === QueuedScroll.INIT) {
|
||||
const initIndex = challenges.findIndex(
|
||||
(challenge) => challenge.id === pathname.split('#')[1]
|
||||
(challenge) => challenge.id === asPath.split('#')[1]
|
||||
);
|
||||
if (initIndex === -1) {
|
||||
queuedScrollRef.current = undefined;
|
||||
@@ -116,7 +112,7 @@ export function Challenges({
|
||||
});
|
||||
queuedScrollRef.current = undefined;
|
||||
}
|
||||
}, [activeIndex, pathname, challenges]);
|
||||
}, [activeIndex, asPath, challenges]);
|
||||
|
||||
const handleChallengeChange = (index: number) => {
|
||||
setActiveIndex(index);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@ export function CodeDiagram({children, flip = false}: CodeDiagramProps) {
|
||||
return child.type === 'img';
|
||||
});
|
||||
const content = Children.toArray(children).map((child: any) => {
|
||||
if (child.props?.['data-mdx-name'] === 'pre') {
|
||||
if (child.type?.mdxName === 'pre') {
|
||||
return (
|
||||
<CodeBlock
|
||||
key={child.key}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useErrorDecoderParams} from '../ErrorDecoderContext';
|
||||
import cn from 'classnames';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
@@ -11,8 +9,8 @@ import {IconDeepDive} from '../Icon/IconDeepDive';
|
||||
import {IconCodeBlock} from '../Icon/IconCodeBlock';
|
||||
import {Button} from '../Button';
|
||||
import {H4} from './Heading';
|
||||
import {useRouter} from 'next/router';
|
||||
import {useEffect, useRef, useState} from 'react';
|
||||
import {usePathname} from 'next/navigation';
|
||||
|
||||
interface ExpandableExampleProps {
|
||||
children: React.ReactNode;
|
||||
@@ -21,23 +19,17 @@ interface ExpandableExampleProps {
|
||||
}
|
||||
|
||||
function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
|
||||
// Validate children using `data-mdx-name`
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children[0].props?.['data-mdx-name'] !== 'h4'
|
||||
) {
|
||||
if (!Array.isArray(children) || children[0].type.mdxName !== 'h4') {
|
||||
throw Error(
|
||||
`Expandable content ${type} is missing a corresponding title at the beginning`
|
||||
);
|
||||
}
|
||||
|
||||
const isDeepDive = type === 'DeepDive';
|
||||
const isExample = type === 'Example';
|
||||
const id = children[0].props.id;
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const shouldAutoExpand = id === pathname.split('#')[1];
|
||||
const {asPath} = useRouter();
|
||||
const shouldAutoExpand = id === asPath.split('#')[1];
|
||||
const queuedExpandRef = useRef<boolean>(shouldAutoExpand);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
@@ -65,7 +57,8 @@ function ExpandableExample({children, excerpt, type}: ExpandableExampleProps) {
|
||||
className="list-none p-8"
|
||||
tabIndex={-1 /* there's a button instead */}
|
||||
onClick={(e) => {
|
||||
// Toggle with a button instead of the whole area
|
||||
// We toggle using a button instead of this whole area,
|
||||
// with an escape case for the header anchor link
|
||||
if (!(e.target instanceof SVGElement)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, {Children} from 'react';
|
||||
|
||||
const IllustrationContext = React.createContext<{
|
||||
isInBlock?: boolean;
|
||||
}>({
|
||||
isInBlock: false,
|
||||
});
|
||||
|
||||
function AuthorCredit({
|
||||
author = 'Rachel Lee Nabors',
|
||||
authorLink = 'https://nearestnabors.com/',
|
||||
}: {
|
||||
author: string;
|
||||
authorLink: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="sr-only group-hover:not-sr-only group-focus-within:not-sr-only hover:sr-only">
|
||||
<p className="bg-card dark:bg-card-dark text-center text-sm text-secondary dark:text-secondary-dark leading-tight p-2 rounded-lg absolute start-1/2 -top-4 -translate-x-1/2 -translate-y-full group-hover:flex group-hover:opacity-100 after:content-[''] after:absolute after:start-1/2 after:top-[95%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-card after:dark:border-t-card-dark opacity-0 transition-opacity duration-300">
|
||||
<cite>
|
||||
Illustrated by{' '}
|
||||
{authorLink ? (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link dark:text-link-dark"
|
||||
href={authorLink}>
|
||||
{author}
|
||||
</a>
|
||||
) : (
|
||||
author
|
||||
)}
|
||||
</cite>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Illustration({
|
||||
caption,
|
||||
src,
|
||||
alt,
|
||||
author,
|
||||
authorLink,
|
||||
}: {
|
||||
caption: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
author: string;
|
||||
authorLink: string;
|
||||
}) {
|
||||
const {isInBlock} = React.useContext(IllustrationContext);
|
||||
|
||||
return (
|
||||
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
|
||||
<figure className="my-8 flex justify-center">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{maxHeight: 300}}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
{caption ? (
|
||||
<figcaption className="text-center leading-tight mt-4">
|
||||
{caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
{!isInBlock && <AuthorCredit author={author} authorLink={authorLink} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isInBlockTrue = {isInBlock: true};
|
||||
|
||||
export function IllustrationBlock({
|
||||
sequential,
|
||||
author,
|
||||
authorLink,
|
||||
children,
|
||||
}: {
|
||||
author: string;
|
||||
authorLink: string;
|
||||
sequential: boolean;
|
||||
children: any;
|
||||
}) {
|
||||
const imageInfos = Children.toArray(children).map(
|
||||
(child: any) => child.props
|
||||
);
|
||||
const images = imageInfos.map((info, index) => (
|
||||
<figure key={index}>
|
||||
<div className="bg-white rounded-lg p-4 flex-1 flex xl:p-6 justify-center items-center my-4">
|
||||
<img
|
||||
className="text-primary"
|
||||
src={info.src}
|
||||
alt={info.alt}
|
||||
height={info.height}
|
||||
/>
|
||||
</div>
|
||||
{info.caption ? (
|
||||
<figcaption className="text-secondary dark:text-secondary-dark text-center leading-tight mt-4">
|
||||
{info.caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
));
|
||||
return (
|
||||
<IllustrationContext.Provider value={isInBlockTrue}>
|
||||
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
|
||||
{sequential ? (
|
||||
<ol className="mdx-illustration-block flex">
|
||||
{images.map((x: any, i: number) => (
|
||||
<li className="flex-1" key={i}>
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="mdx-illustration-block">{images}</div>
|
||||
)}
|
||||
<AuthorCredit author={author} authorLink={authorLink} />
|
||||
</div>
|
||||
</IllustrationContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import cn from 'classnames';
|
||||
import {useContext, type HTMLAttributes} from 'react';
|
||||
import {LinkContext} from './Link';
|
||||
import type {HTMLAttributes} from 'react';
|
||||
|
||||
interface InlineCodeProps {
|
||||
isLink?: boolean;
|
||||
meta?: string;
|
||||
}
|
||||
function InlineCode({...props}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
|
||||
const isLink = useContext(LinkContext);
|
||||
function InlineCode({
|
||||
isLink,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLElement> & InlineCodeProps) {
|
||||
return (
|
||||
<code
|
||||
dir="ltr" // This is needed to prevent the code from inheriting the RTL direction of <html> in case of RTL languages to avoid like `()console.log` to be rendered as `console.log()`
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client';
|
||||
|
||||
// import Link from 'next/link';
|
||||
import Link from './Link';
|
||||
import {useContext, useMemo} from 'react';
|
||||
import {Toc, TocContext, TocItem} from './TocContext';
|
||||
import {UL, LI} from './Primitives';
|
||||
|
||||
type NestedTocRoot = {
|
||||
item: null;
|
||||
children: Array<NestedTocNode>;
|
||||
};
|
||||
|
||||
type NestedTocNode = {
|
||||
item: TocItem;
|
||||
children: Array<NestedTocNode>;
|
||||
};
|
||||
|
||||
function calculateNestedToc(toc: Toc): NestedTocRoot {
|
||||
const currentAncestors = new Map<number, NestedTocNode | NestedTocRoot>();
|
||||
const root: NestedTocRoot = {
|
||||
item: null,
|
||||
children: [],
|
||||
};
|
||||
const startIndex = 1; // Skip "Overview"
|
||||
for (let i = startIndex; i < toc.length; i++) {
|
||||
const item = toc[i];
|
||||
const currentParent: NestedTocNode | NestedTocRoot =
|
||||
currentAncestors.get(item.depth - 1) || root;
|
||||
const node: NestedTocNode = {
|
||||
item,
|
||||
children: [],
|
||||
};
|
||||
currentParent.children.push(node);
|
||||
currentAncestors.set(item.depth, node);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
export function InlineToc() {
|
||||
const toc = useContext(TocContext);
|
||||
const root = useMemo(() => calculateNestedToc(toc), [toc]);
|
||||
if (root.children.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return <InlineTocItem items={root.children} />;
|
||||
}
|
||||
|
||||
function InlineTocItem({items}: {items: Array<NestedTocNode>}) {
|
||||
return (
|
||||
<UL>
|
||||
{items.map((node) => (
|
||||
<LI key={node.item.url}>
|
||||
<Link href={node.item.url}>{node.item.node}</Link>
|
||||
{node.children.length > 0 && <InlineTocItem items={node.children} />}
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Link from './Link';
|
||||
import React from 'react';
|
||||
import {finishedTranslations} from 'utils/finishedTranslations';
|
||||
import {LanguagesContext} from './LanguagesContext';
|
||||
import {UL, LI} from './Primitives';
|
||||
|
||||
type TranslationProgress = 'complete' | 'in-progress';
|
||||
|
||||
export function LanguageList({progress}: {progress: TranslationProgress}) {
|
||||
const allLanguages = React.useContext(LanguagesContext) ?? [];
|
||||
const languages = allLanguages
|
||||
.filter(
|
||||
({code}) =>
|
||||
code !== 'en' &&
|
||||
(progress === 'complete'
|
||||
? finishedTranslations.includes(code)
|
||||
: !finishedTranslations.includes(code))
|
||||
)
|
||||
.sort((a, b) => a.enName.localeCompare(b.enName));
|
||||
return (
|
||||
<UL>
|
||||
{languages.map(({code, name, enName}) => {
|
||||
return (
|
||||
<LI key={code}>
|
||||
<Link href={`https://${code}.react.dev/`}>
|
||||
{enName} ({name})
|
||||
</Link>{' '}
|
||||
—{' '}
|
||||
<Link href={`https://github.com/reactjs/${code}.react.dev`}>
|
||||
Contribute
|
||||
</Link>
|
||||
</LI>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {createContext} from 'react';
|
||||
import {Children, cloneElement} from 'react';
|
||||
import NextLink from 'next/link';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {ExternalLink} from 'components/ExternalLink';
|
||||
|
||||
export const LinkContext = createContext(false);
|
||||
|
||||
function Link({
|
||||
href,
|
||||
className,
|
||||
@@ -20,29 +16,36 @@ function Link({
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
|
||||
const classes =
|
||||
'inline text-link dark:text-link-dark border-b border-link border-opacity-0 hover:border-opacity-100 duration-100 ease-in transition leading-normal';
|
||||
const modifiedChildren = Children.toArray(children).map((child: any) => {
|
||||
if (child.type?.mdxName && child.type?.mdxName === 'inlineCode') {
|
||||
return cloneElement(child, {
|
||||
isLink: true,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
if (!href) {
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
return <a href={href} className={className} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkContext.Provider value={true}>
|
||||
<>
|
||||
{href.startsWith('https://') ? (
|
||||
<ExternalLink href={href} className={cn(classes, className)} {...props}>
|
||||
{children}
|
||||
{modifiedChildren}
|
||||
</ExternalLink>
|
||||
) : href.startsWith('#') ? (
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
<a className={cn(classes, className)} href={href} {...props}>
|
||||
{children}
|
||||
{modifiedChildren}
|
||||
</a>
|
||||
) : (
|
||||
<NextLink href={href} className={cn(classes, className)} {...props}>
|
||||
{children}
|
||||
{modifiedChildren}
|
||||
</NextLink>
|
||||
)}
|
||||
</LinkContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// 'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
// import {Children, useContext, useMemo} from 'react';
|
||||
import {Children, useContext, useMemo} from 'react';
|
||||
import * as React from 'react';
|
||||
import cn from 'classnames';
|
||||
import type {HTMLAttributes} from 'react';
|
||||
@@ -31,13 +29,14 @@ import YouWillLearnCard from './YouWillLearnCard';
|
||||
import {Challenges, Hint, Solution} from './Challenges';
|
||||
import {IconNavArrow} from '../Icon/IconNavArrow';
|
||||
import ButtonLink from 'components/ButtonLink';
|
||||
import {TocContext} from './TocContext';
|
||||
import type {Toc, TocItem} from './TocContext';
|
||||
import {TeamMember} from './TeamMember';
|
||||
import {LanguagesContext} from './LanguagesContext';
|
||||
import {finishedTranslations} from 'utils/finishedTranslations';
|
||||
|
||||
import ErrorDecoder from './ErrorDecoder';
|
||||
import {IconCanary} from '../Icon/IconCanary';
|
||||
import {InlineToc} from './InlineToc';
|
||||
import {Illustration, IllustrationBlock} from './Illustration';
|
||||
import {LanguageList} from './LanguageList';
|
||||
import {Divider, LI, OL, P, Strong, UL} from './Primitives';
|
||||
|
||||
function CodeStep({children, step}: {children: any; step: number}) {
|
||||
return (
|
||||
@@ -61,6 +60,27 @@ function CodeStep({children, step}: {children: any; step: number}) {
|
||||
);
|
||||
}
|
||||
|
||||
const P = (p: HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className="whitespace-pre-wrap my-4" {...p} />
|
||||
);
|
||||
|
||||
const Strong = (strong: HTMLAttributes<HTMLElement>) => (
|
||||
<strong className="font-bold" {...strong} />
|
||||
);
|
||||
|
||||
const OL = (p: HTMLAttributes<HTMLOListElement>) => (
|
||||
<ol className="ms-6 my-3 list-decimal" {...p} />
|
||||
);
|
||||
const LI = (p: HTMLAttributes<HTMLLIElement>) => (
|
||||
<li className="leading-relaxed mb-1" {...p} />
|
||||
);
|
||||
const UL = (p: HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul className="ms-6 my-3 list-disc" {...p} />
|
||||
);
|
||||
|
||||
const Divider = () => (
|
||||
<hr className="my-6 block border-b border-t-0 border-border dark:border-border-dark" />
|
||||
);
|
||||
const Wip = ({children}: {children: React.ReactNode}) => (
|
||||
<ExpandableCallout type="wip">{children}</ExpandableCallout>
|
||||
);
|
||||
@@ -212,6 +232,214 @@ function Recipes(props: any) {
|
||||
return <Challenges {...props} isRecipes={true} />;
|
||||
}
|
||||
|
||||
function AuthorCredit({
|
||||
author = 'Rachel Lee Nabors',
|
||||
authorLink = 'https://nearestnabors.com/',
|
||||
}: {
|
||||
author: string;
|
||||
authorLink: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="sr-only group-hover:not-sr-only group-focus-within:not-sr-only hover:sr-only">
|
||||
<p className="bg-card dark:bg-card-dark text-center text-sm text-secondary dark:text-secondary-dark leading-tight p-2 rounded-lg absolute start-1/2 -top-4 -translate-x-1/2 -translate-y-full group-hover:flex group-hover:opacity-100 after:content-[''] after:absolute after:start-1/2 after:top-[95%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-card after:dark:border-t-card-dark opacity-0 transition-opacity duration-300">
|
||||
<cite>
|
||||
Illustrated by{' '}
|
||||
{authorLink ? (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link dark:text-link-dark"
|
||||
href={authorLink}>
|
||||
{author}
|
||||
</a>
|
||||
) : (
|
||||
author
|
||||
)}
|
||||
</cite>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IllustrationContext = React.createContext<{
|
||||
isInBlock?: boolean;
|
||||
}>({
|
||||
isInBlock: false,
|
||||
});
|
||||
|
||||
function Illustration({
|
||||
caption,
|
||||
src,
|
||||
alt,
|
||||
author,
|
||||
authorLink,
|
||||
}: {
|
||||
caption: string;
|
||||
src: string;
|
||||
alt: string;
|
||||
author: string;
|
||||
authorLink: string;
|
||||
}) {
|
||||
const {isInBlock} = React.useContext(IllustrationContext);
|
||||
|
||||
return (
|
||||
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
|
||||
<figure className="my-8 flex justify-center">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{maxHeight: 300}}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
{caption ? (
|
||||
<figcaption className="text-center leading-tight mt-4">
|
||||
{caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
{!isInBlock && <AuthorCredit author={author} authorLink={authorLink} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isInBlockTrue = {isInBlock: true};
|
||||
|
||||
function IllustrationBlock({
|
||||
sequential,
|
||||
author,
|
||||
authorLink,
|
||||
children,
|
||||
}: {
|
||||
author: string;
|
||||
authorLink: string;
|
||||
sequential: boolean;
|
||||
children: any;
|
||||
}) {
|
||||
const imageInfos = Children.toArray(children).map(
|
||||
(child: any) => child.props
|
||||
);
|
||||
const images = imageInfos.map((info, index) => (
|
||||
<figure key={index}>
|
||||
<div className="bg-white rounded-lg p-4 flex-1 flex xl:p-6 justify-center items-center my-4">
|
||||
<img
|
||||
className="text-primary"
|
||||
src={info.src}
|
||||
alt={info.alt}
|
||||
height={info.height}
|
||||
/>
|
||||
</div>
|
||||
{info.caption ? (
|
||||
<figcaption className="text-secondary dark:text-secondary-dark text-center leading-tight mt-4">
|
||||
{info.caption}
|
||||
</figcaption>
|
||||
) : null}
|
||||
</figure>
|
||||
));
|
||||
return (
|
||||
<IllustrationContext.Provider value={isInBlockTrue}>
|
||||
<div className="relative group before:absolute before:-inset-y-16 before:inset-x-0 my-16 mx-0 2xl:mx-auto max-w-4xl 2xl:max-w-6xl">
|
||||
{sequential ? (
|
||||
<ol className="mdx-illustration-block flex">
|
||||
{images.map((x: any, i: number) => (
|
||||
<li className="flex-1" key={i}>
|
||||
{x}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="mdx-illustration-block">{images}</div>
|
||||
)}
|
||||
<AuthorCredit author={author} authorLink={authorLink} />
|
||||
</div>
|
||||
</IllustrationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type NestedTocRoot = {
|
||||
item: null;
|
||||
children: Array<NestedTocNode>;
|
||||
};
|
||||
|
||||
type NestedTocNode = {
|
||||
item: TocItem;
|
||||
children: Array<NestedTocNode>;
|
||||
};
|
||||
|
||||
function calculateNestedToc(toc: Toc): NestedTocRoot {
|
||||
const currentAncestors = new Map<number, NestedTocNode | NestedTocRoot>();
|
||||
const root: NestedTocRoot = {
|
||||
item: null,
|
||||
children: [],
|
||||
};
|
||||
const startIndex = 1; // Skip "Overview"
|
||||
for (let i = startIndex; i < toc.length; i++) {
|
||||
const item = toc[i];
|
||||
const currentParent: NestedTocNode | NestedTocRoot =
|
||||
currentAncestors.get(item.depth - 1) || root;
|
||||
const node: NestedTocNode = {
|
||||
item,
|
||||
children: [],
|
||||
};
|
||||
currentParent.children.push(node);
|
||||
currentAncestors.set(item.depth, node);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function InlineToc() {
|
||||
const toc = useContext(TocContext);
|
||||
const root = useMemo(() => calculateNestedToc(toc), [toc]);
|
||||
if (root.children.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return <InlineTocItem items={root.children} />;
|
||||
}
|
||||
|
||||
function InlineTocItem({items}: {items: Array<NestedTocNode>}) {
|
||||
return (
|
||||
<UL>
|
||||
{items.map((node) => (
|
||||
<LI key={node.item.url}>
|
||||
<Link href={node.item.url}>{node.item.text}</Link>
|
||||
{node.children.length > 0 && <InlineTocItem items={node.children} />}
|
||||
</LI>
|
||||
))}
|
||||
</UL>
|
||||
);
|
||||
}
|
||||
|
||||
type TranslationProgress = 'complete' | 'in-progress';
|
||||
|
||||
function LanguageList({progress}: {progress: TranslationProgress}) {
|
||||
const allLanguages = React.useContext(LanguagesContext) ?? [];
|
||||
const languages = allLanguages
|
||||
.filter(
|
||||
({code}) =>
|
||||
code !== 'en' &&
|
||||
(progress === 'complete'
|
||||
? finishedTranslations.includes(code)
|
||||
: !finishedTranslations.includes(code))
|
||||
)
|
||||
.sort((a, b) => a.enName.localeCompare(b.enName));
|
||||
return (
|
||||
<UL>
|
||||
{languages.map(({code, name, enName}) => {
|
||||
return (
|
||||
<LI key={code}>
|
||||
<Link href={`https://${code}.react.dev/`}>
|
||||
{enName} ({name})
|
||||
</Link>{' '}
|
||||
—{' '}
|
||||
<Link href={`https://github.com/reactjs/${code}.react.dev`}>
|
||||
Contribute
|
||||
</Link>
|
||||
</LI>
|
||||
);
|
||||
})}
|
||||
</UL>
|
||||
);
|
||||
}
|
||||
|
||||
function YouTubeIframe(props: any) {
|
||||
return (
|
||||
<div className="relative h-0 overflow-hidden pt-[56.25%]">
|
||||
@@ -232,22 +460,7 @@ function Image(props: any) {
|
||||
return <img alt={alt} className="max-w-[calc(min(700px,100%))]" {...rest} />;
|
||||
}
|
||||
|
||||
function annotateMDXComponents(
|
||||
components: Record<string, React.ElementType>
|
||||
): Record<string, React.ElementType> {
|
||||
return Object.entries(components).reduce((acc, [key, Component]) => {
|
||||
acc[key] = (props) => <Component {...props} data-mdx-name={key} />;
|
||||
acc[key].displayName = `Annotated(${key})`; // Optional, for debugging
|
||||
return acc;
|
||||
}, {} as Record<string, React.ElementType>);
|
||||
}
|
||||
|
||||
export const MDXComponentsToc = annotateMDXComponents({
|
||||
a: Link,
|
||||
code: InlineCode,
|
||||
});
|
||||
|
||||
export const MDXComponents = annotateMDXComponents({
|
||||
export const MDXComponents = {
|
||||
p: P,
|
||||
strong: Strong,
|
||||
blockquote: Blockquote,
|
||||
@@ -316,4 +529,11 @@ export const MDXComponents = annotateMDXComponents({
|
||||
CodeStep,
|
||||
YouTubeIframe,
|
||||
ErrorDecoder,
|
||||
});
|
||||
};
|
||||
|
||||
for (let key in MDXComponents) {
|
||||
if (MDXComponents.hasOwnProperty(key)) {
|
||||
const MDXComponent: any = (MDXComponents as any)[key];
|
||||
MDXComponent.mdxName = key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ interface PackageImportProps {
|
||||
|
||||
export function PackageImport({children}: PackageImportProps) {
|
||||
const terminal = Children.toArray(children).filter((child: any) => {
|
||||
return child.props?.['data-mdx-name'] !== 'pre';
|
||||
return child.type?.mdxName !== 'pre';
|
||||
});
|
||||
const code = Children.toArray(children).map((child: any, i: number) => {
|
||||
if (child.props?.['data-mdx-name'] === 'pre') {
|
||||
if (child.type?.mdxName === 'pre') {
|
||||
return (
|
||||
<CodeBlock
|
||||
{...child.props}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import {HTMLAttributes} from 'react';
|
||||
|
||||
export const P = (p: HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className="whitespace-pre-wrap my-4" {...p} />
|
||||
);
|
||||
|
||||
export const Strong = (strong: HTMLAttributes<HTMLElement>) => (
|
||||
<strong className="font-bold" {...strong} />
|
||||
);
|
||||
|
||||
export const OL = (p: HTMLAttributes<HTMLOListElement>) => (
|
||||
<ol className="ms-6 my-3 list-decimal" {...p} />
|
||||
);
|
||||
export const LI = (p: HTMLAttributes<HTMLLIElement>) => (
|
||||
<li className="leading-relaxed mb-1" {...p} />
|
||||
);
|
||||
export const UL = (p: HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul className="ms-6 my-3 list-disc" {...p} />
|
||||
);
|
||||
|
||||
export const Divider = () => (
|
||||
<hr className="my-6 block border-b border-t-0 border-border dark:border-border-dark" />
|
||||
);
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -12,22 +12,19 @@ export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];
|
||||
export const createFileMap = (codeSnippets: any) => {
|
||||
return codeSnippets.reduce(
|
||||
(result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {
|
||||
// TODO: actually fix this
|
||||
if (
|
||||
(codeSnippet.type as any).mdxName !== 'pre' &&
|
||||
codeSnippet.type !== 'pre'
|
||||
) {
|
||||
return result;
|
||||
}
|
||||
const {props} = (
|
||||
codeSnippet.props as PropsWithChildren<{
|
||||
children: ReactElement<
|
||||
HTMLAttributes<HTMLDivElement> & {
|
||||
meta?: string;
|
||||
'data-mdx-name'?: string;
|
||||
}
|
||||
HTMLAttributes<HTMLDivElement> & {meta?: string}
|
||||
>;
|
||||
}>
|
||||
).children;
|
||||
|
||||
if (props?.['data-mdx-name'] !== 'code') {
|
||||
return result;
|
||||
}
|
||||
|
||||
let filePath; // path in the folder structure
|
||||
let fileHidden = false; // if the file is available as a tab
|
||||
let fileActive = false; // if the file tab is shown by default
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import {Children, memo} from 'react';
|
||||
import InlineCode from './InlineCode';
|
||||
import Sandpack from './Sandpack';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {ReactNode} from 'react';
|
||||
|
||||
export type TocItem = {
|
||||
url: string;
|
||||
node: ReactNode;
|
||||
text: ReactNode;
|
||||
depth: number;
|
||||
};
|
||||
export type Toc = Array<TocItem>;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export function ScrollHandler() {
|
||||
useEffect(() => {
|
||||
// Taken from StackOverflow. Trying to detect both Safari desktop and mobile.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// This is kind of a lie.
|
||||
// We still rely on the manual Next.js scrollRestoration logic.
|
||||
// However, we *also* don't want Safari grey screen during the back swipe gesture.
|
||||
// Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.
|
||||
history.scrollRestoration = 'auto';
|
||||
} else {
|
||||
// For other browsers, let Next.js set scrollRestoration to 'manual'.
|
||||
// It seems to work better for Chrome and Firefox which don't animate the back swipe.
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import Router from 'next/router';
|
||||
import {lazy, useEffect} from 'react';
|
||||
import * as React from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
@@ -111,7 +111,6 @@ export function Search({
|
||||
},
|
||||
}: SearchProps) {
|
||||
useDocSearchKeyboardEvents({isOpen, onOpen, onClose});
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -128,7 +127,7 @@ export function Search({
|
||||
onClose={onClose}
|
||||
navigator={{
|
||||
navigate({itemUrl}: any) {
|
||||
router.push(itemUrl);
|
||||
Router.push(itemUrl);
|
||||
},
|
||||
}}
|
||||
transformItems={(items: any[]) => {
|
||||
|
||||
185
src/components/Seo.tsx
Normal file
185
src/components/Seo.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import Head from 'next/head';
|
||||
import {withRouter, Router} from 'next/router';
|
||||
import {siteConfig} from '../siteConfig';
|
||||
import {finishedTranslations} from 'utils/finishedTranslations';
|
||||
|
||||
export interface SeoProps {
|
||||
title: string;
|
||||
titleForTitleTag: undefined | string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
// jsonld?: JsonLDType | Array<JsonLDType>;
|
||||
children?: React.ReactNode;
|
||||
isHomePage: boolean;
|
||||
searchOrder?: number;
|
||||
}
|
||||
|
||||
// If you are a maintainer of a language fork,
|
||||
// deployedTranslations has been moved to src/utils/finishedTranslations.ts.
|
||||
|
||||
function getDomain(languageCode: string): string {
|
||||
const subdomain = languageCode === 'en' ? '' : languageCode + '.';
|
||||
return subdomain + 'react.dev';
|
||||
}
|
||||
|
||||
export const Seo = withRouter(
|
||||
({
|
||||
title,
|
||||
titleForTitleTag,
|
||||
image = '/images/og-default.png',
|
||||
router,
|
||||
children,
|
||||
isHomePage,
|
||||
searchOrder,
|
||||
}: SeoProps & {router: Router}) => {
|
||||
const siteDomain = getDomain(siteConfig.languageCode);
|
||||
const canonicalUrl = `https://${siteDomain}${
|
||||
router.asPath.split(/[\?\#]/)[0]
|
||||
}`;
|
||||
// Allow setting a different title for Google results
|
||||
const pageTitle =
|
||||
(titleForTitleTag ?? title) + (isHomePage ? '' : ' – React');
|
||||
// Twitter's meta parser is not very good.
|
||||
const twitterTitle = pageTitle.replace(/[<>]/g, '');
|
||||
let description = isHomePage
|
||||
? 'React is the library for web and native user interfaces. Build user interfaces out of individual pieces called components written in JavaScript. React is designed to let you seamlessly combine components written by independent people, teams, and organizations.'
|
||||
: 'The library for web and native user interfaces';
|
||||
return (
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{title != null && <title key="title">{pageTitle}</title>}
|
||||
{isHomePage && (
|
||||
// Let Google figure out a good description for each page.
|
||||
<meta name="description" key="description" content={description} />
|
||||
)}
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
<link
|
||||
rel="alternate"
|
||||
href={canonicalUrl.replace(siteDomain, getDomain('en'))}
|
||||
hrefLang="x-default"
|
||||
/>
|
||||
{finishedTranslations.map((languageCode) => (
|
||||
<link
|
||||
key={'alt-' + languageCode}
|
||||
rel="alternate"
|
||||
hrefLang={languageCode}
|
||||
href={canonicalUrl.replace(siteDomain, getDomain(languageCode))}
|
||||
/>
|
||||
))}
|
||||
<meta property="fb:app_id" content="623268441017527" />
|
||||
<meta property="og:type" key="og:type" content="website" />
|
||||
<meta property="og:url" key="og:url" content={canonicalUrl} />
|
||||
{title != null && (
|
||||
<meta property="og:title" content={pageTitle} key="og:title" />
|
||||
)}
|
||||
{description != null && (
|
||||
<meta
|
||||
property="og:description"
|
||||
key="og:description"
|
||||
content={description}
|
||||
/>
|
||||
)}
|
||||
<meta
|
||||
property="og:image"
|
||||
key="og:image"
|
||||
content={`https://${siteDomain}${image}`}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:card"
|
||||
key="twitter:card"
|
||||
content="summary_large_image"
|
||||
/>
|
||||
<meta name="twitter:site" key="twitter:site" content="@reactjs" />
|
||||
<meta name="twitter:creator" key="twitter:creator" content="@reactjs" />
|
||||
{title != null && (
|
||||
<meta
|
||||
name="twitter:title"
|
||||
key="twitter:title"
|
||||
content={twitterTitle}
|
||||
/>
|
||||
)}
|
||||
{description != null && (
|
||||
<meta
|
||||
name="twitter:description"
|
||||
key="twitter:description"
|
||||
content={description}
|
||||
/>
|
||||
)}
|
||||
<meta
|
||||
name="twitter:image"
|
||||
key="twitter:image"
|
||||
content={`https://${siteDomain}${image}`}
|
||||
/>
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content="sIlAGs48RulR4DdP95YSWNKZIEtCqQmRjzn-Zq-CcD0"
|
||||
/>
|
||||
{searchOrder != null && (
|
||||
<meta name="algolia-search-order" content={'' + searchOrder} />
|
||||
)}
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/Source-Code-Pro-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Display_W_Md.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Display_W_SBd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Display_W_Bd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Text_W_Md.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Text_W_Bd.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Text_W_Rg.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://react.dev/fonts/Optimistic_Text_W_It.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
{children}
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,52 +0,0 @@
|
||||
function ThemeInlineScript() {
|
||||
function setTheme(newTheme) {
|
||||
window.__theme = newTheme;
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (newTheme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
var preferredTheme;
|
||||
try {
|
||||
preferredTheme = localStorage.getItem('theme');
|
||||
} catch (err) {}
|
||||
|
||||
window.__setPreferredTheme = function (newTheme) {
|
||||
preferredTheme = newTheme;
|
||||
setTheme(newTheme);
|
||||
try {
|
||||
localStorage.setItem('theme', newTheme);
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
var initialTheme = preferredTheme;
|
||||
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
if (!initialTheme) {
|
||||
initialTheme = darkQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
setTheme(initialTheme);
|
||||
|
||||
darkQuery.addEventListener('change', function (e) {
|
||||
if (!preferredTheme) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
|
||||
document.documentElement.classList.add(
|
||||
window.navigator.platform.includes('Mac') ? 'platform-mac' : 'platform-win'
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeScript() {
|
||||
return (
|
||||
<script
|
||||
id="theme-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${ThemeInlineScript.toString()})()`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
function UwuInlineScript() {
|
||||
try {
|
||||
let logShown = false;
|
||||
function setUwu(isUwu) {
|
||||
try {
|
||||
if (isUwu) {
|
||||
localStorage.setItem('uwu', true);
|
||||
document.documentElement.classList.add('uwu');
|
||||
if (!logShown) {
|
||||
console.log('uwu mode! turn off with ?uwu=0');
|
||||
console.log(
|
||||
'logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/ServiceLogos'
|
||||
);
|
||||
logShown = true;
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('uwu');
|
||||
document.documentElement.classList.remove('uwu');
|
||||
console.log('uwu mode off. turn on with ?uwu');
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
window.__setUwu = setUwu;
|
||||
function checkQueryParam() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get('uwu');
|
||||
switch (value) {
|
||||
case '':
|
||||
case 'true':
|
||||
case '1':
|
||||
return true;
|
||||
case 'false':
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function checkLocalStorage() {
|
||||
try {
|
||||
return localStorage.getItem('uwu') === 'true';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const uwuQueryParam = checkQueryParam();
|
||||
if (uwuQueryParam != null) {
|
||||
setUwu(uwuQueryParam);
|
||||
} else if (checkLocalStorage()) {
|
||||
document.documentElement.classList.add('uwu');
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
export function UwuScript() {
|
||||
return (
|
||||
<script
|
||||
id="uwu-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${UwuInlineScript.toString()})()`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,5 @@ React documentation is written and maintained by the [React team](/community/tea
|
||||
* [Rick Hanlon](https://twitter.com/rickhanlonii): site development
|
||||
* [Harish Kumar](https://www.strek.in/): development and maintenance
|
||||
* [Luna Ruan](https://twitter.com/lunaruan): sandbox improvements
|
||||
* [Jimmy Lai](https://twitter.com/feedthejim): site development
|
||||
|
||||
We'd also like to thank countless alpha testers and community members who gave us feedback along the way.
|
||||
|
||||
@@ -298,4 +298,4 @@ See the first blog post: [Why did we build React?](https://legacy.reactjs.org/bl
|
||||
|
||||
React was open sourced at Facebook Seattle in 2013:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/XxVg_s8xAms?si=466vSJrnXTn05j9A" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe>
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/XxVg_s8xAms?si=466vSJrnXTn05j9A" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
@@ -2,9 +2,40 @@
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {useRouter} from 'next/router';
|
||||
import {useState, useRef, useEffect} from 'react';
|
||||
|
||||
const usePendingRoute = () => {
|
||||
// TODO: @feedthejim - Implement usePendingRoute when App Router supports tapping into the transition state
|
||||
return null;
|
||||
const {events} = useRouter();
|
||||
const [pendingRoute, setPendingRoute] = useState<string | null>(null);
|
||||
const currentRoute = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
let routeTransitionTimer: any = null;
|
||||
|
||||
const handleRouteChangeStart = (url: string) => {
|
||||
clearTimeout(routeTransitionTimer);
|
||||
routeTransitionTimer = setTimeout(() => {
|
||||
if (currentRoute.current !== url) {
|
||||
currentRoute.current = url;
|
||||
setPendingRoute(url);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
const handleRouteChangeComplete = () => {
|
||||
setPendingRoute(null);
|
||||
clearTimeout(routeTransitionTimer);
|
||||
};
|
||||
events.on('routeChangeStart', handleRouteChangeStart);
|
||||
events.on('routeChangeComplete', handleRouteChangeComplete);
|
||||
|
||||
return () => {
|
||||
events.off('routeChangeStart', handleRouteChangeStart);
|
||||
events.off('routeChangeComplete', handleRouteChangeComplete);
|
||||
clearTimeout(routeTransitionTimer);
|
||||
};
|
||||
}, [events]);
|
||||
|
||||
return pendingRoute;
|
||||
};
|
||||
|
||||
export default usePendingRoute;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
export function register() {
|
||||
if (
|
||||
process.env.NODE_ENV === 'development' &&
|
||||
process.env.NEXT_RUNTIME === 'nodejs'
|
||||
) {
|
||||
// watch for changes in the ./src/content directory
|
||||
// and trigger an HMR update when a change is detected via a custom WebSocket setup
|
||||
const chokidar = require('chokidar');
|
||||
const path = require('path');
|
||||
const ws = require('ws');
|
||||
|
||||
const wsServer = new ws.Server({
|
||||
port: 3001,
|
||||
});
|
||||
|
||||
function triggerRefresh() {
|
||||
wsServer.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify({event: 'refresh'}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// the process is in .next so we need to go up two level
|
||||
const contentDir = path.resolve(__dirname, '../../src/content');
|
||||
const watcher = chokidar.watch(contentDir, {
|
||||
ignoreInitial: true,
|
||||
});
|
||||
|
||||
watcher.on('all', () => {
|
||||
triggerRefresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,12 @@
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import {MDXComponents} from 'components/MDX/MDXComponents';
|
||||
import sidebarLearn from '../sidebarLearn.json';
|
||||
import {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
|
||||
|
||||
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Page
|
||||
toc={[]}
|
||||
pathname="/404"
|
||||
section="unknown"
|
||||
meta={{title: 'Not Found'}}
|
||||
routeTree={sidebarLearn as RouteItem}>
|
||||
<Page toc={[]} meta={{title: 'Not Found'}} routeTree={sidebarLearn}>
|
||||
<MaxWidth>
|
||||
<Intro>
|
||||
<P>This page doesn’t exist.</P>
|
||||
@@ -34,11 +27,3 @@ export default function NotFound() {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({}: {}) {
|
||||
return generateSeoMetadata({
|
||||
title: 'Not Found',
|
||||
isHomePage: false,
|
||||
path: '/404',
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
@@ -7,18 +5,14 @@
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import {MDXComponents} from 'components/MDX/MDXComponents';
|
||||
import sidebarLearn from '../sidebarLearn.json';
|
||||
import {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {generateMetadata as generateSeoMetadata} from 'utils/generateMetadata';
|
||||
|
||||
const {Intro, MaxWidth, p: P, a: A} = MDXComponents;
|
||||
|
||||
export default function Error() {
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Page
|
||||
section="unknown"
|
||||
toc={[]}
|
||||
pathname="/500"
|
||||
routeTree={sidebarLearn as RouteItem}
|
||||
routeTree={sidebarLearn}
|
||||
meta={{title: 'Something Went Wrong'}}>
|
||||
<MaxWidth>
|
||||
<Intro>
|
||||
@@ -35,11 +29,3 @@ export default function Error() {
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({}: {}) {
|
||||
return generateSeoMetadata({
|
||||
title: 'Something Went Wrong',
|
||||
isHomePage: false,
|
||||
path: '/500',
|
||||
});
|
||||
}
|
||||
179
src/pages/[[...markdownPath]].js
Normal file
179
src/pages/[[...markdownPath]].js
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {Fragment, useMemo} from 'react';
|
||||
import {useRouter} from 'next/router';
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import sidebarHome from '../sidebarHome.json';
|
||||
import sidebarLearn from '../sidebarLearn.json';
|
||||
import sidebarReference from '../sidebarReference.json';
|
||||
import sidebarCommunity from '../sidebarCommunity.json';
|
||||
import sidebarBlog from '../sidebarBlog.json';
|
||||
import {MDXComponents} from 'components/MDX/MDXComponents';
|
||||
import compileMDX from 'utils/compileMDX';
|
||||
import {generateRssFeed} from '../utils/rss';
|
||||
|
||||
export default function Layout({content, toc, meta, languages}) {
|
||||
const parsedContent = useMemo(
|
||||
() => JSON.parse(content, reviveNodeOnClient),
|
||||
[content]
|
||||
);
|
||||
const parsedToc = useMemo(() => JSON.parse(toc, reviveNodeOnClient), [toc]);
|
||||
const section = useActiveSection();
|
||||
let routeTree;
|
||||
switch (section) {
|
||||
case 'home':
|
||||
case 'unknown':
|
||||
routeTree = sidebarHome;
|
||||
break;
|
||||
case 'learn':
|
||||
routeTree = sidebarLearn;
|
||||
break;
|
||||
case 'reference':
|
||||
routeTree = sidebarReference;
|
||||
break;
|
||||
case 'community':
|
||||
routeTree = sidebarCommunity;
|
||||
break;
|
||||
case 'blog':
|
||||
routeTree = sidebarBlog;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Page
|
||||
toc={parsedToc}
|
||||
routeTree={routeTree}
|
||||
meta={meta}
|
||||
section={section}
|
||||
languages={languages}>
|
||||
{parsedContent}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
function useActiveSection() {
|
||||
const {asPath} = useRouter();
|
||||
const cleanedPath = asPath.split(/[\?\#]/)[0];
|
||||
if (cleanedPath === '/') {
|
||||
return 'home';
|
||||
} else if (cleanedPath.startsWith('/reference')) {
|
||||
return 'reference';
|
||||
} else if (asPath.startsWith('/learn')) {
|
||||
return 'learn';
|
||||
} else if (asPath.startsWith('/community')) {
|
||||
return 'community';
|
||||
} else if (asPath.startsWith('/blog')) {
|
||||
return 'blog';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize a client React tree from JSON.
|
||||
function reviveNodeOnClient(parentPropertyName, val) {
|
||||
if (Array.isArray(val) && val[0] == '$r') {
|
||||
// Assume it's a React element.
|
||||
let Type = val[1];
|
||||
let key = val[2];
|
||||
if (key == null) {
|
||||
key = parentPropertyName; // Index within a parent.
|
||||
}
|
||||
let props = val[3];
|
||||
if (Type === 'wrapper') {
|
||||
Type = Fragment;
|
||||
props = {children: props.children};
|
||||
}
|
||||
if (Type in MDXComponents) {
|
||||
Type = MDXComponents[Type];
|
||||
}
|
||||
if (!Type) {
|
||||
console.error('Unknown type: ' + Type);
|
||||
Type = Fragment;
|
||||
}
|
||||
return <Type key={key} {...props} />;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// Put MDX output into JSON for client.
|
||||
export async function getStaticProps(context) {
|
||||
generateRssFeed();
|
||||
const fs = require('fs');
|
||||
const rootDir = process.cwd() + '/src/content/';
|
||||
|
||||
// Read MDX from the file.
|
||||
let path = (context.params.markdownPath || []).join('/') || 'index';
|
||||
let mdx;
|
||||
try {
|
||||
mdx = fs.readFileSync(rootDir + path + '.md', 'utf8');
|
||||
} catch {
|
||||
mdx = fs.readFileSync(rootDir + path + '/index.md', 'utf8');
|
||||
}
|
||||
|
||||
const {toc, content, meta, languages} = await compileMDX(mdx, path, {});
|
||||
return {
|
||||
props: {
|
||||
toc,
|
||||
content,
|
||||
meta,
|
||||
languages,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Collect all MDX files for static generation.
|
||||
export async function getStaticPaths() {
|
||||
const {promisify} = require('util');
|
||||
const {resolve} = require('path');
|
||||
const fs = require('fs');
|
||||
const readdir = promisify(fs.readdir);
|
||||
const stat = promisify(fs.stat);
|
||||
const rootDir = process.cwd() + '/src/content';
|
||||
|
||||
// Find all MD files recursively.
|
||||
async function getFiles(dir) {
|
||||
const subdirs = await readdir(dir);
|
||||
const files = await Promise.all(
|
||||
subdirs.map(async (subdir) => {
|
||||
const res = resolve(dir, subdir);
|
||||
return (await stat(res)).isDirectory()
|
||||
? getFiles(res)
|
||||
: res.slice(rootDir.length + 1);
|
||||
})
|
||||
);
|
||||
return (
|
||||
files
|
||||
.flat()
|
||||
// ignores `errors/*.md`, they will be handled by `pages/errors/[errorCode].tsx`
|
||||
.filter((file) => file.endsWith('.md') && !file.startsWith('errors/'))
|
||||
);
|
||||
}
|
||||
|
||||
// 'foo/bar/baz.md' -> ['foo', 'bar', 'baz']
|
||||
// 'foo/bar/qux/index.md' -> ['foo', 'bar', 'qux']
|
||||
function getSegments(file) {
|
||||
let segments = file.slice(0, -3).replace(/\\/g, '/').split('/');
|
||||
if (segments[segments.length - 1] === 'index') {
|
||||
segments.pop();
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
const files = await getFiles(rootDir);
|
||||
|
||||
const paths = files.map((file) => ({
|
||||
params: {
|
||||
markdownPath: getSegments(file),
|
||||
// ^^^ CAREFUL HERE.
|
||||
// If you rename markdownPath, update patches/next-remote-watch.patch too.
|
||||
// Otherwise you'll break Fast Refresh for all MD files.
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
paths: paths,
|
||||
fallback: false,
|
||||
};
|
||||
}
|
||||
58
src/pages/_app.tsx
Normal file
58
src/pages/_app.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {AppProps} from 'next/app';
|
||||
import {useRouter} from 'next/router';
|
||||
|
||||
import '@docsearch/css';
|
||||
import '../styles/algolia.css';
|
||||
import '../styles/index.css';
|
||||
import '../styles/sandpack.css';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const terminationEvent = 'onpagehide' in window ? 'pagehide' : 'unload';
|
||||
window.addEventListener(terminationEvent, function () {
|
||||
// @ts-ignore
|
||||
gtag('event', 'timing', {
|
||||
event_label: 'JS Dependencies',
|
||||
event: 'unload',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default function MyApp({Component, pageProps}: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
// Taken from StackOverflow. Trying to detect both Safari desktop and mobile.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// This is kind of a lie.
|
||||
// We still rely on the manual Next.js scrollRestoration logic.
|
||||
// However, we *also* don't want Safari grey screen during the back swipe gesture.
|
||||
// Seems like it doesn't hurt to enable auto restore *and* Next.js logic at the same time.
|
||||
history.scrollRestoration = 'auto';
|
||||
} else {
|
||||
// For other browsers, let Next.js set scrollRestoration to 'manual'.
|
||||
// It seems to work better for Chrome and Firefox which don't animate the back swipe.
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
const cleanedUrl = url.split(/[\?\#]/)[0];
|
||||
// @ts-ignore
|
||||
gtag('event', 'pageview', {
|
||||
event_label: cleanedUrl,
|
||||
});
|
||||
};
|
||||
router.events.on('routeChangeComplete', handleRouteChange);
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
158
src/pages/_document.tsx
Normal file
158
src/pages/_document.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {Html, Head, Main, NextScript} from 'next/document';
|
||||
import {siteConfig} from '../siteConfig';
|
||||
|
||||
const MyDocument = () => {
|
||||
return (
|
||||
<Html lang={siteConfig.languageCode} dir={siteConfig.isRTL ? 'rtl' : 'ltr'}>
|
||||
<Head />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#404756" />
|
||||
<meta name="msapplication-TileColor" content="#2b5797" />
|
||||
<meta name="theme-color" content="#23272f" />
|
||||
<script
|
||||
async
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_TRACKING_ID}`}
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${process.env.NEXT_PUBLIC_GA_TRACKING_ID}');`,
|
||||
}}
|
||||
/>
|
||||
<body className="font-text font-medium antialiased text-lg bg-wash dark:bg-wash-dark text-secondary dark:text-secondary-dark leading-base">
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function () {
|
||||
try {
|
||||
let logShown = false;
|
||||
function setUwu(isUwu) {
|
||||
try {
|
||||
if (isUwu) {
|
||||
localStorage.setItem('uwu', true);
|
||||
document.documentElement.classList.add('uwu');
|
||||
if (!logShown) {
|
||||
console.log('uwu mode! turn off with ?uwu=0');
|
||||
console.log('logo credit to @sawaratsuki1004 via https://github.com/SAWARATSUKI/ServiceLogos');
|
||||
logShown = true;
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('uwu');
|
||||
document.documentElement.classList.remove('uwu');
|
||||
console.log('uwu mode off. turn on with ?uwu');
|
||||
}
|
||||
} catch (err) { }
|
||||
}
|
||||
window.__setUwu = setUwu;
|
||||
function checkQueryParam() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const value = params.get('uwu');
|
||||
switch(value) {
|
||||
case '':
|
||||
case 'true':
|
||||
case '1':
|
||||
return true;
|
||||
case 'false':
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function checkLocalStorage() {
|
||||
try {
|
||||
return localStorage.getItem('uwu') === 'true';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const uwuQueryParam = checkQueryParam();
|
||||
if (uwuQueryParam != null) {
|
||||
setUwu(uwuQueryParam);
|
||||
} else if (checkLocalStorage()) {
|
||||
document.documentElement.classList.add('uwu');
|
||||
}
|
||||
} catch (err) { }
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function () {
|
||||
function setTheme(newTheme) {
|
||||
window.__theme = newTheme;
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (newTheme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
var preferredTheme;
|
||||
try {
|
||||
preferredTheme = localStorage.getItem('theme');
|
||||
} catch (err) { }
|
||||
|
||||
window.__setPreferredTheme = function(newTheme) {
|
||||
preferredTheme = newTheme;
|
||||
setTheme(newTheme);
|
||||
try {
|
||||
localStorage.setItem('theme', newTheme);
|
||||
} catch (err) { }
|
||||
};
|
||||
|
||||
var initialTheme = preferredTheme;
|
||||
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
if (!initialTheme) {
|
||||
initialTheme = darkQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
setTheme(initialTheme);
|
||||
|
||||
darkQuery.addEventListener('change', function (e) {
|
||||
if (!preferredTheme) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
|
||||
// Detect whether the browser is Mac to display platform specific content
|
||||
// An example of such content can be the keyboard shortcut displayed in the search bar
|
||||
document.documentElement.classList.add(
|
||||
window.navigator.platform.includes('Mac')
|
||||
? "platform-mac"
|
||||
: "platform-win"
|
||||
);
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyDocument;
|
||||
153
src/pages/errors/[errorCode].tsx
Normal file
153
src/pages/errors/[errorCode].tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import {Fragment, useMemo} from 'react';
|
||||
import {Page} from 'components/Layout/Page';
|
||||
import {MDXComponents} from 'components/MDX/MDXComponents';
|
||||
import sidebarLearn from 'sidebarLearn.json';
|
||||
import type {RouteItem} from 'components/Layout/getRouteMeta';
|
||||
import {GetStaticPaths, GetStaticProps, InferGetStaticPropsType} from 'next';
|
||||
import {ErrorDecoderContext} from 'components/ErrorDecoderContext';
|
||||
import compileMDX from 'utils/compileMDX';
|
||||
|
||||
interface ErrorDecoderProps {
|
||||
errorCode: string | null;
|
||||
errorMessage: string | null;
|
||||
content: string;
|
||||
toc: string;
|
||||
meta: any;
|
||||
}
|
||||
|
||||
export default function ErrorDecoderPage({
|
||||
errorMessage,
|
||||
errorCode,
|
||||
content,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const parsedContent = useMemo<React.ReactNode>(
|
||||
() => JSON.parse(content, reviveNodeOnClient),
|
||||
[content]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorDecoderContext.Provider value={{errorMessage, errorCode}}>
|
||||
<Page
|
||||
toc={[]}
|
||||
meta={{
|
||||
title: errorCode
|
||||
? `Minified React error #${errorCode}`
|
||||
: 'Minified Error Decoder',
|
||||
}}
|
||||
routeTree={sidebarLearn as RouteItem}
|
||||
section="unknown">
|
||||
<div className="whitespace-pre-line">{parsedContent}</div>
|
||||
{/* <MaxWidth>
|
||||
<P>
|
||||
We highly recommend using the development build locally when debugging
|
||||
your app since it tracks additional debug info and provides helpful
|
||||
warnings about potential problems in your apps, but if you encounter
|
||||
an exception while using the production build, this page will
|
||||
reassemble the original error message.
|
||||
</P>
|
||||
<ErrorDecoder />
|
||||
</MaxWidth> */}
|
||||
</Page>
|
||||
</ErrorDecoderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Deserialize a client React tree from JSON.
|
||||
function reviveNodeOnClient(parentPropertyName: unknown, val: any) {
|
||||
if (Array.isArray(val) && val[0] == '$r') {
|
||||
// Assume it's a React element.
|
||||
let Type = val[1];
|
||||
let key = val[2];
|
||||
if (key == null) {
|
||||
key = parentPropertyName; // Index within a parent.
|
||||
}
|
||||
let props = val[3];
|
||||
if (Type === 'wrapper') {
|
||||
Type = Fragment;
|
||||
props = {children: props.children};
|
||||
}
|
||||
if (Type in MDXComponents) {
|
||||
Type = MDXComponents[Type as keyof typeof MDXComponents];
|
||||
}
|
||||
if (!Type) {
|
||||
console.error('Unknown type: ' + Type);
|
||||
Type = Fragment;
|
||||
}
|
||||
return <Type key={key} {...props} />;
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Next.js Page Router doesn't have a way to cache specific data fetching request.
|
||||
* But since Next.js uses limited number of workers, keep "cachedErrorCodes" as a
|
||||
* module level memory cache can reduce the number of requests down to once per worker.
|
||||
*
|
||||
* TODO: use `next/unstable_cache` when migrating to Next.js App Router
|
||||
*/
|
||||
let cachedErrorCodes: Record<string, string> | null = null;
|
||||
|
||||
export const getStaticProps: GetStaticProps<ErrorDecoderProps> = async ({
|
||||
params,
|
||||
}) => {
|
||||
const errorCodes: {[key: string]: string} = (cachedErrorCodes ||= await (
|
||||
await fetch(
|
||||
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
|
||||
)
|
||||
).json());
|
||||
|
||||
const code = typeof params?.errorCode === 'string' ? params?.errorCode : null;
|
||||
if (code && !errorCodes[code]) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const rootDir = process.cwd() + '/src/content/errors';
|
||||
|
||||
// Read MDX from the file.
|
||||
let path = params?.errorCode || 'index';
|
||||
let mdx;
|
||||
try {
|
||||
mdx = fs.readFileSync(rootDir + '/' + path + '.md', 'utf8');
|
||||
} catch {
|
||||
// if [errorCode].md is not found, fallback to generic.md
|
||||
mdx = fs.readFileSync(rootDir + '/generic.md', 'utf8');
|
||||
}
|
||||
|
||||
const {content, toc, meta} = await compileMDX(mdx, path, {code, errorCodes});
|
||||
|
||||
return {
|
||||
props: {
|
||||
content,
|
||||
toc,
|
||||
meta,
|
||||
errorCode: code,
|
||||
errorMessage: code ? errorCodes[code] : null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
/**
|
||||
* Fetch error codes from GitHub
|
||||
*/
|
||||
const errorCodes = (cachedErrorCodes ||= await (
|
||||
await fetch(
|
||||
'https://raw.githubusercontent.com/facebook/react/main/scripts/error-codes/codes.json'
|
||||
)
|
||||
).json());
|
||||
|
||||
const paths = Object.keys(errorCodes).map((code) => ({
|
||||
params: {
|
||||
errorCode: code,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: 'blocking',
|
||||
};
|
||||
};
|
||||
3
src/pages/errors/index.tsx
Normal file
3
src/pages/errors/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import ErrorDecoderPage from './[errorCode]';
|
||||
export default ErrorDecoderPage;
|
||||
export {getStaticProps} from './[errorCode]';
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -27,7 +28,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_MdIt.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_MdIt.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -35,7 +37,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_SBd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_SBd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -43,7 +46,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_SBdIt.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_SBdIt.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -51,7 +55,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -59,7 +64,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_W_BdIt.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_W_BdIt.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -67,7 +73,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_Rg.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_Rg.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -75,7 +82,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_It.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_It.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -83,7 +91,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -91,7 +100,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_MdIt.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_MdIt.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -99,7 +109,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -107,7 +118,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_W_BdIt.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_W_BdIt.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -117,7 +129,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Arbc_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -126,7 +139,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Arbc_W_SBd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_SBd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -135,7 +149,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Arbc_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Arbc_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -144,7 +159,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Arbc_W_Rg.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Rg.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -153,7 +169,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Arbc_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -162,7 +179,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Arbc_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Arbc_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -173,7 +191,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Cyrl_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -182,7 +201,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Cyrl_W_SBd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_SBd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -191,7 +211,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Cyrl_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Cyrl_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -200,7 +221,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Cyrl_W_Rg.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Rg.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -209,7 +231,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Cyrl_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -218,7 +241,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Cyrl_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Cyrl_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -229,7 +253,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Deva_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -239,7 +264,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Deva_W_SBd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_SBd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -249,7 +275,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Deva_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Deva_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -259,7 +286,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Deva_W_Rg.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Rg.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -269,7 +297,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Deva_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -279,7 +308,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Deva_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Deva_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -291,7 +321,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Viet_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -300,7 +331,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Viet_W_SBd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_SBd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -309,7 +341,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Display';
|
||||
src: url('/fonts/Optimistic_Display_Viet_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Display_Viet_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -318,7 +351,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Viet_W_Rg.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Rg.woff2')
|
||||
format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -327,7 +361,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Viet_W_Md.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Md.woff2')
|
||||
format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -336,7 +371,8 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Optimistic Text';
|
||||
src: url('/fonts/Optimistic_Text_Viet_W_Bd.woff2') format('woff2');
|
||||
src: url('https://react.dev/fonts/Optimistic_Text_Viet_W_Bd.woff2')
|
||||
format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
||||
168
src/utils/compileMDX.ts
Normal file
168
src/utils/compileMDX.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import {LanguageItem} from 'components/MDX/LanguagesContext';
|
||||
import {MDXComponents} from 'components/MDX/MDXComponents';
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ~~~~ IMPORTANT: BUMP THIS IF YOU CHANGE ANY CODE BELOW ~~~
|
||||
const DISK_CACHE_BREAKER = 10;
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
export default async function compileMDX(
|
||||
mdx: string,
|
||||
path: string | string[],
|
||||
params: {[key: string]: any}
|
||||
): Promise<{content: string; toc: string; meta: any}> {
|
||||
const fs = require('fs');
|
||||
const {
|
||||
prepareMDX,
|
||||
PREPARE_MDX_CACHE_BREAKER,
|
||||
} = require('../utils/prepareMDX');
|
||||
const mdxComponentNames = Object.keys(MDXComponents);
|
||||
|
||||
// See if we have a cached output first.
|
||||
const {FileStore, stableHash} = require('metro-cache');
|
||||
const store = new FileStore({
|
||||
root: process.cwd() + '/node_modules/.cache/react-docs-mdx/',
|
||||
});
|
||||
const hash = Buffer.from(
|
||||
stableHash({
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ~~~~ IMPORTANT: Everything that the code below may rely on.
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
mdx,
|
||||
...params,
|
||||
mdxComponentNames,
|
||||
DISK_CACHE_BREAKER,
|
||||
PREPARE_MDX_CACHE_BREAKER,
|
||||
lockfile: fs.readFileSync(process.cwd() + '/yarn.lock', 'utf8'),
|
||||
})
|
||||
);
|
||||
const cached = await store.get(hash);
|
||||
if (cached) {
|
||||
console.log(
|
||||
'Reading compiled MDX for /' + path + ' from ./node_modules/.cache/'
|
||||
);
|
||||
return cached;
|
||||
}
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
console.log(
|
||||
'Cache miss for MDX for /' + path + ' from ./node_modules/.cache/'
|
||||
);
|
||||
}
|
||||
|
||||
// If we don't add these fake imports, the MDX compiler
|
||||
// will insert a bunch of opaque components we can't introspect.
|
||||
// This will break the prepareMDX() call below.
|
||||
let mdxWithFakeImports =
|
||||
mdx +
|
||||
'\n\n' +
|
||||
mdxComponentNames
|
||||
.map((key) => 'import ' + key + ' from "' + key + '";\n')
|
||||
.join('\n');
|
||||
|
||||
// Turn the MDX we just read into some JS we can execute.
|
||||
const {remarkPlugins} = require('../../plugins/markdownToHtml');
|
||||
const {compile: compileMdx} = await import('@mdx-js/mdx');
|
||||
const visit = (await import('unist-util-visit')).default;
|
||||
const jsxCode = await compileMdx(mdxWithFakeImports, {
|
||||
remarkPlugins: [
|
||||
...remarkPlugins,
|
||||
(await import('remark-gfm')).default,
|
||||
(await import('remark-frontmatter')).default,
|
||||
],
|
||||
rehypePlugins: [
|
||||
// Support stuff like ```js App.js {1-5} active by passing it through.
|
||||
function rehypeMetaAsAttributes() {
|
||||
return (tree) => {
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
// @ts-expect-error -- tagName is a valid property
|
||||
node.tagName === 'code' &&
|
||||
node.data &&
|
||||
node.data.meta
|
||||
) {
|
||||
// @ts-expect-error -- properties is a valid property
|
||||
node.properties.meta = node.data.meta;
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
],
|
||||
});
|
||||
const {transform} = require('@babel/core');
|
||||
const jsCode = await transform(jsxCode, {
|
||||
plugins: ['@babel/plugin-transform-modules-commonjs'],
|
||||
presets: ['@babel/preset-react'],
|
||||
}).code;
|
||||
|
||||
// Prepare environment for MDX.
|
||||
let fakeExports = {};
|
||||
const fakeRequire = (name: string) => {
|
||||
if (name === 'react/jsx-runtime') {
|
||||
return require('react/jsx-runtime');
|
||||
} else {
|
||||
// For each fake MDX import, give back the string component name.
|
||||
// It will get serialized later.
|
||||
return name;
|
||||
}
|
||||
};
|
||||
const evalJSCode = new Function('require', 'exports', jsCode);
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// THIS IS A BUILD-TIME EVAL. NEVER DO THIS WITH UNTRUSTED MDX (LIKE FROM CMS)!!!
|
||||
// In this case it's okay because anyone who can edit our MDX can also edit this file.
|
||||
evalJSCode(fakeRequire, fakeExports);
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
// @ts-expect-error -- default exports is existed after eval
|
||||
const reactTree = fakeExports.default({});
|
||||
|
||||
// Pre-process MDX output and serialize it.
|
||||
let {toc, children} = prepareMDX(reactTree.props.children);
|
||||
if (path === 'index') {
|
||||
toc = [];
|
||||
}
|
||||
|
||||
// Parse Frontmatter headers from MDX.
|
||||
const fm = require('gray-matter');
|
||||
const meta = fm(mdx).data;
|
||||
|
||||
// Load the list of translated languages conditionally.
|
||||
let languages: Array<LanguageItem> | null = null;
|
||||
if (typeof path === 'string' && path.endsWith('/translations')) {
|
||||
languages = await (
|
||||
await fetch(
|
||||
'https://raw.githubusercontent.com/reactjs/translations.react.dev/main/langs/langs.json'
|
||||
)
|
||||
).json(); // { code: string; name: string; enName: string}[]
|
||||
}
|
||||
|
||||
const output = {
|
||||
content: JSON.stringify(children, stringifyNodeOnServer),
|
||||
toc: JSON.stringify(toc, stringifyNodeOnServer),
|
||||
meta,
|
||||
languages,
|
||||
};
|
||||
|
||||
// Serialize a server React tree node to JSON.
|
||||
function stringifyNodeOnServer(key: unknown, val: any) {
|
||||
if (
|
||||
val != null &&
|
||||
val.$$typeof === Symbol.for('react.transitional.element')
|
||||
) {
|
||||
// Remove fake MDX props.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const {mdxType, originalType, parentName, ...cleanProps} = val.props;
|
||||
return [
|
||||
'$r',
|
||||
typeof val.type === 'string' ? val.type : mdxType,
|
||||
val.key,
|
||||
cleanProps,
|
||||
];
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache it on the disk.
|
||||
await store.set(hash, output);
|
||||
return output;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import {FileStore, stableHash} from 'metro-cache';
|
||||
import grayMatter from 'gray-matter';
|
||||
import {compile, run} from '@mdx-js/mdx';
|
||||
import * as runtime from 'react/jsx-runtime';
|
||||
import {remarkPlugins} from '../../plugins/markdownToHtml';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import {MDXComponents, MDXComponentsToc} from '../components/MDX/MDXComponents';
|
||||
import {MaxWidthWrapperPlugin} from './mdx/MaxWidthWrapperPlugin';
|
||||
import {ExtractedTOC, TOCExtractorPlugin} from './mdx/TOCExtractorPlugin';
|
||||
import {MetaAttributesPlugin} from './mdx/MetaAttributesPlugin';
|
||||
|
||||
const DISK_CACHE_BREAKER = 13;
|
||||
const CACHE_PATH = `${process.cwd()}/node_modules/.cache/react-docs-mdx/`;
|
||||
const LOCKFILE_PATH = `${process.cwd()}/yarn.lock`;
|
||||
|
||||
type Params = {[key: string]: any};
|
||||
type MDXResult = {
|
||||
content: React.ReactNode;
|
||||
toc: ExtractedTOC[];
|
||||
meta: any;
|
||||
};
|
||||
|
||||
type CachedResult = {
|
||||
code: string;
|
||||
toc: ExtractedTOC[];
|
||||
meta: any;
|
||||
};
|
||||
|
||||
async function readFromCache(
|
||||
store: FileStore,
|
||||
hash: Buffer,
|
||||
path: string | string[]
|
||||
): Promise<CachedResult | null> {
|
||||
try {
|
||||
const cached = await store.get(hash);
|
||||
if (cached) {
|
||||
return JSON.parse(cached.toString());
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Cache read failed for /${path}:`, error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function writeToCache(
|
||||
store: FileStore,
|
||||
hash: Buffer,
|
||||
result: CachedResult,
|
||||
path: string | string[]
|
||||
): Promise<void> {
|
||||
try {
|
||||
await store.set(hash, Buffer.from(JSON.stringify(result)));
|
||||
} catch (error) {
|
||||
console.warn(`Cache write failed for /${path}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMDX(
|
||||
mdx: string,
|
||||
path: string | string[],
|
||||
params: Params
|
||||
): Promise<MDXResult> {
|
||||
const store = new FileStore({root: CACHE_PATH});
|
||||
const hash = Buffer.from(
|
||||
stableHash({
|
||||
...params,
|
||||
mdx,
|
||||
mdxComponentNames: Object.keys(MDXComponents),
|
||||
DISK_CACHE_BREAKER,
|
||||
lockfile: fs.readFileSync(LOCKFILE_PATH, 'utf8'),
|
||||
})
|
||||
);
|
||||
|
||||
const cachedResult = await readFromCache(store, hash, path);
|
||||
if (cachedResult) {
|
||||
const {code, meta, toc} = cachedResult;
|
||||
const {default: MDXContent} = await run(code, {
|
||||
...runtime,
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
const tocWithMDX = await getTransformedToc(toc);
|
||||
return {
|
||||
content: <MDXContent components={{...MDXComponents}} />,
|
||||
toc: tocWithMDX,
|
||||
meta,
|
||||
};
|
||||
}
|
||||
|
||||
const compiled = await compile(mdx, {
|
||||
remarkPlugins: [
|
||||
...remarkPlugins,
|
||||
remarkGfm,
|
||||
remarkFrontmatter,
|
||||
MaxWidthWrapperPlugin,
|
||||
TOCExtractorPlugin,
|
||||
],
|
||||
rehypePlugins: [MetaAttributesPlugin],
|
||||
outputFormat: 'function-body',
|
||||
});
|
||||
|
||||
const {data: meta} = grayMatter(mdx);
|
||||
const toc = compiled.data.toc as ExtractedTOC[];
|
||||
const result: CachedResult = {
|
||||
code: String(compiled),
|
||||
toc,
|
||||
meta,
|
||||
};
|
||||
|
||||
await writeToCache(store, hash, result, path);
|
||||
|
||||
const tocWithMDX = await getTransformedToc(toc);
|
||||
|
||||
const {default: MDXContent} = await run(result.code, {
|
||||
...runtime,
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
return {
|
||||
content: <MDXContent components={{...MDXComponents}} />,
|
||||
toc: tocWithMDX,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
|
||||
async function getTransformedToc(toc: ExtractedTOC[]): Promise<ExtractedTOC[]> {
|
||||
return await Promise.all(
|
||||
toc.map(async (item) => {
|
||||
if (typeof item.node !== 'string') {
|
||||
return item;
|
||||
}
|
||||
|
||||
const compiled = await compile(item.node, {
|
||||
outputFormat: 'function-body',
|
||||
});
|
||||
|
||||
const {default: MDXContent} = await run(compiled, {
|
||||
...runtime,
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
item.node = <MDXContent components={{...MDXComponentsToc}} />;
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import {Metadata} from 'next';
|
||||
import {siteConfig} from '../siteConfig';
|
||||
import {finishedTranslations} from 'utils/finishedTranslations';
|
||||
|
||||
export interface SeoProps {
|
||||
title: string;
|
||||
titleForTitleTag?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
isHomePage: boolean;
|
||||
searchOrder?: number;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function getDomain(languageCode: string): string {
|
||||
const subdomain = languageCode === 'en' ? '' : languageCode + '.';
|
||||
return subdomain + 'react.dev';
|
||||
}
|
||||
export function generateMetadata({
|
||||
title,
|
||||
titleForTitleTag,
|
||||
image,
|
||||
isHomePage,
|
||||
description: customDescription,
|
||||
searchOrder,
|
||||
path,
|
||||
}: SeoProps): Metadata {
|
||||
const siteDomain = getDomain(siteConfig.languageCode);
|
||||
const canonicalUrl = `https://${siteDomain}${path.split(/[\?\#]/)[0]}`;
|
||||
|
||||
const pageTitle =
|
||||
(titleForTitleTag ?? title) + (isHomePage ? '' : ' – React');
|
||||
const twitterTitle = pageTitle.replace(/[<>]/g, '');
|
||||
|
||||
const description = isHomePage
|
||||
? 'React is the library for web and native user interfaces...'
|
||||
: customDescription ?? 'The library for web and native user interfaces';
|
||||
|
||||
const alternateLanguages = {
|
||||
'x-default': canonicalUrl.replace(siteDomain, getDomain('en')),
|
||||
...Object.fromEntries(
|
||||
finishedTranslations.map((languageCode) => [
|
||||
languageCode,
|
||||
canonicalUrl.replace(siteDomain, getDomain(languageCode)),
|
||||
])
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
title: pageTitle,
|
||||
description: isHomePage ? description : undefined,
|
||||
alternates: {
|
||||
canonical: canonicalUrl,
|
||||
languages: alternateLanguages,
|
||||
},
|
||||
openGraph: {
|
||||
title: pageTitle,
|
||||
description,
|
||||
url: canonicalUrl,
|
||||
images: [
|
||||
{url: `https://${siteDomain}${image || '/images/og-default.png'}`},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: twitterTitle,
|
||||
description,
|
||||
images: [`https://${siteDomain}${image || '/images/og-default.png'}`],
|
||||
},
|
||||
other: {
|
||||
...(searchOrder != null && {
|
||||
'algolia-search-order': searchOrder.toString(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import {Root, RootContent} from 'mdast';
|
||||
import {u} from 'unist-builder';
|
||||
|
||||
export function MaxWidthWrapperPlugin() {
|
||||
const fullWidthTypes = [
|
||||
'Sandpack',
|
||||
'FullWidth',
|
||||
'Illustration',
|
||||
'IllustrationBlock',
|
||||
'Challenges',
|
||||
'Recipes',
|
||||
];
|
||||
|
||||
return (tree: Root) => {
|
||||
const newChildren: RootContent[] = [];
|
||||
let wrapQueue: RootContent[] = [];
|
||||
|
||||
function flushWrapper() {
|
||||
if (wrapQueue.length > 0) {
|
||||
newChildren.push(
|
||||
u('mdxJsxFlowElement', {
|
||||
name: 'MaxWidth',
|
||||
attributes: [],
|
||||
children: wrapQueue,
|
||||
} as any)
|
||||
);
|
||||
wrapQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of tree.children) {
|
||||
if (
|
||||
// @ts-expect-error
|
||||
fullWidthTypes.includes(node.name) &&
|
||||
// @ts-expect-error: mdxJsxFlowElement
|
||||
node.type === 'mdxJsxFlowElement'
|
||||
) {
|
||||
flushWrapper();
|
||||
newChildren.push(node);
|
||||
} else {
|
||||
wrapQueue.push(node);
|
||||
}
|
||||
}
|
||||
flushWrapper();
|
||||
|
||||
tree.children = newChildren;
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {Root} from 'mdast';
|
||||
import visit from 'unist-util-visit';
|
||||
|
||||
// Support stuff like ```js App.js {1-5} active by passing it through.
|
||||
export function MetaAttributesPlugin() {
|
||||
return (tree: Root) => {
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
// @ts-expect-error -- tagName is a valid property
|
||||
node.tagName === 'code' &&
|
||||
node.data &&
|
||||
node.data.meta
|
||||
) {
|
||||
// @ts-expect-error -- properties is a valid property
|
||||
node.properties.meta = node.data.meta;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import visit from 'unist-util-visit';
|
||||
import {Node} from 'unist';
|
||||
|
||||
interface HeadingNode extends Node {
|
||||
type: 'heading';
|
||||
depth: number;
|
||||
children: Array<{
|
||||
type: string;
|
||||
value?: string;
|
||||
}>;
|
||||
data?: {
|
||||
hProperties?: {
|
||||
id?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface MDXJsxFlowElementNode extends Node {
|
||||
type: 'mdxJsxFlowElement';
|
||||
name: string;
|
||||
attributes?: Array<{
|
||||
name: string;
|
||||
value?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ExtractedTOC {
|
||||
url: string;
|
||||
node: string | React.ReactNode;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface PluginOptions {
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
export function TOCExtractorPlugin({maxDepth = 3}: PluginOptions = {}) {
|
||||
return (tree: Node, file: any) => {
|
||||
const toc: ExtractedTOC[] = [];
|
||||
|
||||
visit(tree, (node: Node) => {
|
||||
// Standard markdown headings
|
||||
if (node.type === 'heading') {
|
||||
const headingNode = node as HeadingNode;
|
||||
if (headingNode.depth > maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mdxSource = file.value
|
||||
.slice(
|
||||
// @ts-ignore
|
||||
node.children[0].position!.start.offset,
|
||||
// @ts-ignore
|
||||
node.children[0].position!.end.offset
|
||||
)
|
||||
.trim();
|
||||
|
||||
const text = headingNode.children
|
||||
.filter((child) => child.type === 'text' && child.value)
|
||||
.map((child) => child.value!)
|
||||
.join('');
|
||||
|
||||
const id =
|
||||
headingNode.data?.hProperties?.id ||
|
||||
text.toLowerCase().replace(/\s+/g, '-');
|
||||
|
||||
toc.push({
|
||||
depth: headingNode.depth,
|
||||
node: mdxSource,
|
||||
url: `#${id}`,
|
||||
});
|
||||
}
|
||||
|
||||
// MDX custom components (e.g., <TeamMember>)
|
||||
else if (node.type === 'mdxJsxFlowElement') {
|
||||
const mdxNode = node as MDXJsxFlowElementNode;
|
||||
switch (mdxNode.name) {
|
||||
case 'TeamMember': {
|
||||
let name = 'Team Member';
|
||||
let permalink = 'team-member';
|
||||
|
||||
if (Array.isArray(mdxNode.attributes)) {
|
||||
for (const attr of mdxNode.attributes) {
|
||||
if (attr.name === 'name' && attr.value) {
|
||||
name = attr.value;
|
||||
} else if (attr.name === 'permalink' && attr.value) {
|
||||
permalink = attr.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toc.push({
|
||||
url: `#${permalink}`,
|
||||
depth: 3,
|
||||
node: name,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Challenges':
|
||||
toc.push({
|
||||
url: '#challenges',
|
||||
depth: 2,
|
||||
node: 'Challenges',
|
||||
});
|
||||
break;
|
||||
case 'Recap':
|
||||
toc.push({
|
||||
url: '#recap',
|
||||
depth: 2,
|
||||
node: 'Recap',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Insert "Overview" at the top if there's at least one heading
|
||||
if (toc.length > 0) {
|
||||
toc.unshift({
|
||||
url: '#',
|
||||
node: 'Overview',
|
||||
depth: 2,
|
||||
});
|
||||
}
|
||||
|
||||
file.data.toc = toc;
|
||||
};
|
||||
}
|
||||
117
src/utils/prepareMDX.js
Normal file
117
src/utils/prepareMDX.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
|
||||
import {Children} from 'react';
|
||||
|
||||
// TODO: This logic could be in MDX plugins instead.
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
export const PREPARE_MDX_CACHE_BREAKER = 3;
|
||||
// !!! IMPORTANT !!! Bump this if you change any logic.
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
export function prepareMDX(rawChildren) {
|
||||
const toc = getTableOfContents(rawChildren, /* depth */ 10);
|
||||
const children = wrapChildrenInMaxWidthContainers(rawChildren);
|
||||
return {toc, children};
|
||||
}
|
||||
|
||||
function wrapChildrenInMaxWidthContainers(children) {
|
||||
// Auto-wrap everything except a few types into
|
||||
// <MaxWidth> wrappers. Keep reusing the same
|
||||
// wrapper as long as we can until we meet
|
||||
// a full-width section which interrupts it.
|
||||
let fullWidthTypes = [
|
||||
'Sandpack',
|
||||
'FullWidth',
|
||||
'Illustration',
|
||||
'IllustrationBlock',
|
||||
'Challenges',
|
||||
'Recipes',
|
||||
];
|
||||
let wrapQueue = [];
|
||||
let finalChildren = [];
|
||||
function flushWrapper(key) {
|
||||
if (wrapQueue.length > 0) {
|
||||
const Wrapper = 'MaxWidth';
|
||||
finalChildren.push(<Wrapper key={key}>{wrapQueue}</Wrapper>);
|
||||
wrapQueue = [];
|
||||
}
|
||||
}
|
||||
function handleChild(child, key) {
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
if (typeof child !== 'object') {
|
||||
wrapQueue.push(child);
|
||||
return;
|
||||
}
|
||||
if (fullWidthTypes.includes(child.type)) {
|
||||
flushWrapper(key);
|
||||
finalChildren.push(child);
|
||||
} else {
|
||||
wrapQueue.push(child);
|
||||
}
|
||||
}
|
||||
Children.forEach(children, handleChild);
|
||||
flushWrapper('last');
|
||||
return finalChildren;
|
||||
}
|
||||
|
||||
function getTableOfContents(children, depth) {
|
||||
const anchors = [];
|
||||
extractHeaders(children, depth, anchors);
|
||||
if (anchors.length > 0) {
|
||||
anchors.unshift({
|
||||
url: '#',
|
||||
text: 'Overview',
|
||||
depth: 2,
|
||||
});
|
||||
}
|
||||
return anchors;
|
||||
}
|
||||
|
||||
const headerTypes = new Set([
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'Challenges',
|
||||
'Recap',
|
||||
'TeamMember',
|
||||
]);
|
||||
function extractHeaders(children, depth, out) {
|
||||
for (const child of Children.toArray(children)) {
|
||||
if (child.type && headerTypes.has(child.type)) {
|
||||
let header;
|
||||
if (child.type === 'Challenges') {
|
||||
header = {
|
||||
url: '#challenges',
|
||||
depth: 2,
|
||||
text: 'Challenges',
|
||||
};
|
||||
} else if (child.type === 'Recap') {
|
||||
header = {
|
||||
url: '#recap',
|
||||
depth: 2,
|
||||
text: 'Recap',
|
||||
};
|
||||
} else if (child.type === 'TeamMember') {
|
||||
header = {
|
||||
url: '#' + child.props.permalink,
|
||||
depth: 3,
|
||||
text: child.props.name,
|
||||
};
|
||||
} else {
|
||||
header = {
|
||||
url: '#' + child.props.id,
|
||||
depth: (child.type && parseInt(child.type.replace('h', ''), 0)) ?? 0,
|
||||
text: child.props.children,
|
||||
};
|
||||
}
|
||||
out.push(header);
|
||||
} else if (child.children && depth > 0) {
|
||||
extractHeaders(child.children, depth - 1, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/utils/rss.js
Normal file
82
src/utils/rss.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*/
|
||||
const Feed = require('rss');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const matter = require('gray-matter');
|
||||
|
||||
const getAllFiles = function (dirPath, arrayOfFiles) {
|
||||
const files = fs.readdirSync(dirPath);
|
||||
|
||||
arrayOfFiles = arrayOfFiles || [];
|
||||
|
||||
files.forEach(function (file) {
|
||||
if (fs.statSync(dirPath + '/' + file).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(dirPath + '/' + file, arrayOfFiles);
|
||||
} else {
|
||||
arrayOfFiles.push(path.join(dirPath, '/', file));
|
||||
}
|
||||
});
|
||||
|
||||
return arrayOfFiles;
|
||||
};
|
||||
|
||||
exports.generateRssFeed = function () {
|
||||
const feed = new Feed({
|
||||
title: 'React Blog',
|
||||
description:
|
||||
'This blog is the official source for the updates from the React team. Anything important, including release notes or deprecation notices, will be posted here first.',
|
||||
feed_url: 'https://react.dev/rss.xml',
|
||||
site_url: 'https://react.dev/',
|
||||
language: 'en',
|
||||
favicon: 'https://react.dev/favicon.ico',
|
||||
pubDate: new Date(),
|
||||
generator: 'react.dev rss module',
|
||||
});
|
||||
|
||||
const dirPath = path.join(process.cwd(), 'src/content/blog');
|
||||
const filesByOldest = getAllFiles(dirPath);
|
||||
const files = filesByOldest.reverse();
|
||||
|
||||
for (const filePath of files) {
|
||||
const id = path.basename(filePath);
|
||||
if (id !== 'index.md') {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const {data} = matter(content);
|
||||
const slug = filePath.split('/').slice(-4).join('/').replace('.md', '');
|
||||
|
||||
if (data.title == null || data.title.trim() === '') {
|
||||
throw new Error(
|
||||
`${id}: Blog posts must include a title in the metadata, for RSS feeds`
|
||||
);
|
||||
}
|
||||
if (data.author == null || data.author.trim() === '') {
|
||||
throw new Error(
|
||||
`${id}: Blog posts must include an author in the metadata, for RSS feeds`
|
||||
);
|
||||
}
|
||||
if (data.date == null || data.date.trim() === '') {
|
||||
throw new Error(
|
||||
`${id}: Blog posts must include a date in the metadata, for RSS feeds`
|
||||
);
|
||||
}
|
||||
if (data.description == null || data.description.trim() === '') {
|
||||
throw new Error(
|
||||
`${id}: Blog posts must include a description in the metadata, for RSS feeds`
|
||||
);
|
||||
}
|
||||
|
||||
feed.item({
|
||||
id,
|
||||
title: data.title,
|
||||
author: data.author || '',
|
||||
date: new Date(data.date),
|
||||
url: `https://react.dev/blog/${slug}`,
|
||||
description: data.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync('./public/rss.xml', feed.xml({indent: true}));
|
||||
};
|
||||
@@ -9,7 +9,6 @@ module.exports = {
|
||||
content: [
|
||||
'./src/components/**/*.{js,ts,jsx,tsx}',
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx}',
|
||||
'./src/app/**/*.{js,ts,jsx,tsx}',
|
||||
'./src/styles/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
@@ -18,20 +22,14 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "src",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"types.d.ts",
|
||||
"src/components/UwuScript.jsx"
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
8
types.d.ts
vendored
8
types.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
declare module 'metro-cache' {
|
||||
export class FileStore {
|
||||
constructor(options: {root: string});
|
||||
get(hash: Buffer): Promise<Buffer | null>;
|
||||
set(hash: Buffer, value: Buffer): Promise<void>;
|
||||
}
|
||||
export function stableHash(obj: any): string;
|
||||
}
|
||||
142
yarn.lock
142
yarn.lock
@@ -1225,10 +1225,10 @@
|
||||
unist-util-visit "^4.0.0"
|
||||
vfile "^5.0.0"
|
||||
|
||||
"@next/env@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0-canary.33.tgz#5cd769cca64e09564e80817b6b6aeaba472ea9e9"
|
||||
integrity sha512-y3EPM+JYKU8t2K+i6bc0QrotEZVGpqu9eVjprj4cfS8QZyZcL54s+W9aGB0TBuGavU9tQdZ50W186+toeMV+hw==
|
||||
"@next/env@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.0.tgz#35b00a5f60ff10dc275182928c325d25c29379ae"
|
||||
integrity sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==
|
||||
|
||||
"@next/eslint-plugin-next@12.0.3":
|
||||
version "12.0.3"
|
||||
@@ -1237,45 +1237,45 @@
|
||||
dependencies:
|
||||
glob "7.1.7"
|
||||
|
||||
"@next/swc-darwin-arm64@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0-canary.33.tgz#946b9fa766575baf7577ea21b70105d5fefc86ed"
|
||||
integrity sha512-+fCdK2KmR6lWoCTk1fSd5pvbiLZHfZF+D/Xdz3xrXw+pbnBtXWLKQrPT0bCtDseMxD31qcOywq5mAApvI3EGpA==
|
||||
"@next/swc-darwin-arm64@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz#30cb89220e719244c9fa7391641e515a078ade46"
|
||||
integrity sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==
|
||||
|
||||
"@next/swc-darwin-x64@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0-canary.33.tgz#8878b319cd3d3f90648097d5d76b54b1db02d4f0"
|
||||
integrity sha512-GrrU+tSmeBRow+7bnn7i5M96g3tc28hPH5t5Y65qUXGmmrZwGZN1e1d+8QbXPdAGkvjEPcOkUNQuQVpp1qpYPA==
|
||||
"@next/swc-darwin-x64@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz#c24c4f5d1016dd161da32049305b0ddddfc80951"
|
||||
integrity sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0-canary.33.tgz#0bc4f61ccbb4e7424f9acb215f0d7e0674456f5e"
|
||||
integrity sha512-8RnGxnUpASHoUf6aHUifmZom5b4Ow5nTdCib/CNYXZ6VLuL5ocvmr+DXs/SKzi9h8OHR7JkLwKXHCcF8WyscSg==
|
||||
"@next/swc-linux-arm64-gnu@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz#08ed540ecdac74426a624cc7d736dc709244b004"
|
||||
integrity sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0-canary.33.tgz#15fa3982f382dc6c51ac17d47111b6525deb7f94"
|
||||
integrity sha512-COyE0LzMuLBZSR+Z/TOGilyJPdwSU588Vt0+o8GoECkoDEnjyuO2s2nHa2kDAcEfUEPkhlo0tErU3mF+8AVOTQ==
|
||||
"@next/swc-linux-arm64-musl@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz#dfddbd40087d018266aa92515ec5b3e251efa6dd"
|
||||
integrity sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0-canary.33.tgz#94d06dcb6f116c470d9d6a7ddda0a29ab23ce80b"
|
||||
integrity sha512-3Y9lqJs+ftU9jgbLdCtvAvF8MNJsJYGMH7icb8QMs1+yOyHHbmwkZoElKdjwfUWzQ2sX28ywp73GWq4HbrsoUg==
|
||||
"@next/swc-linux-x64-gnu@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz#a7b5373a1b28c0acecbc826a3790139fc0d899e5"
|
||||
integrity sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0-canary.33.tgz#daaa3610648afe7440f3672d11352ecd5109d2df"
|
||||
integrity sha512-FS9iA+RkZlhdWGQEKtsplVBXIYZJUn5nsRB+1UY46b3uaL6dDypu13ODaSwYuAwXGgkrZBVF9AFO3y4biBnPlA==
|
||||
"@next/swc-linux-x64-musl@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz#b82a29903ee2f12d8b64163ddf208ac519869550"
|
||||
integrity sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0-canary.33.tgz#e704113f42449f30ee0d81337ff9c27177cad75c"
|
||||
integrity sha512-Ji9CtBbUx06qvvN/rPohJN2FEFGsUv26F50f2nMRYRwrq3POXDjloGOiRocrjU0ty/cUzCz71qTUfKdmv/ajmg==
|
||||
"@next/swc-win32-arm64-msvc@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz#98deae6cb1fccfb6a600e9faa6aa714402a9ab9a"
|
||||
integrity sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.2.0-canary.33":
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0-canary.33.tgz#d719ea46ba61f3a97678dd0b290147488ea974ba"
|
||||
integrity sha512-hjdbGnkwIZ8zN2vlS6lNsEJO37HRtcEGimzfkruBMsi/DwJBqkJvZbNC/XCJy3HFcU58igncqV52p1IPjmAJAw==
|
||||
"@next/swc-win32-x64-msvc@15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz#4b04a6a667c41fecdc63db57dd71ca7e84d0946b"
|
||||
integrity sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
@@ -1656,13 +1656,6 @@
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/mdast@^4.0.4":
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6"
|
||||
integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
|
||||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/mdurl@^1.0.0":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz"
|
||||
@@ -1731,11 +1724,6 @@
|
||||
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/unist@^3.0.0":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
|
||||
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.36.2":
|
||||
version "5.36.2"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz"
|
||||
@@ -2435,10 +2423,15 @@ camelcase-css@^2.0.1:
|
||||
resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz"
|
||||
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
|
||||
|
||||
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001688:
|
||||
version "1.0.30001695"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz"
|
||||
integrity sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==
|
||||
caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001579:
|
||||
version "1.0.30001636"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz#b15f52d2bdb95fad32c2f53c0b68032b85188a78"
|
||||
integrity sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==
|
||||
|
||||
caniuse-lite@^1.0.30001688:
|
||||
version "1.0.30001692"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz#4585729d95e6b95be5b439da6ab55250cd125bf9"
|
||||
integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
|
||||
|
||||
ccount@^1.0.0:
|
||||
version "1.1.0"
|
||||
@@ -2522,13 +2515,6 @@ chokidar@^3.4.0, chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
chokidar@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
|
||||
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
|
||||
dependencies:
|
||||
readdirp "^4.0.1"
|
||||
|
||||
ci-info@^3.2.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz"
|
||||
@@ -5812,12 +5798,12 @@ next-tick@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz"
|
||||
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
|
||||
|
||||
next@^15.2.0-canary.33:
|
||||
version "15.2.0-canary.33"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.2.0-canary.33.tgz#a866edb669618a5a7aac5035895255eb0c9fb189"
|
||||
integrity sha512-WF8QLeYkakuYwksdWY/F+Bi8tNJfIbiSYk9hCmldn9sNp1lU3lqI1hrW1ynbcMSaXC+qQEr7yol2OdvVZ4nZYQ==
|
||||
next@15.1.0:
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.0.tgz#be847cf67ac94ae23b57f3ea6d10642f3fc1ad69"
|
||||
integrity sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==
|
||||
dependencies:
|
||||
"@next/env" "15.2.0-canary.33"
|
||||
"@next/env" "15.1.0"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
@@ -5825,14 +5811,14 @@ next@^15.2.0-canary.33:
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.2.0-canary.33"
|
||||
"@next/swc-darwin-x64" "15.2.0-canary.33"
|
||||
"@next/swc-linux-arm64-gnu" "15.2.0-canary.33"
|
||||
"@next/swc-linux-arm64-musl" "15.2.0-canary.33"
|
||||
"@next/swc-linux-x64-gnu" "15.2.0-canary.33"
|
||||
"@next/swc-linux-x64-musl" "15.2.0-canary.33"
|
||||
"@next/swc-win32-arm64-msvc" "15.2.0-canary.33"
|
||||
"@next/swc-win32-x64-msvc" "15.2.0-canary.33"
|
||||
"@next/swc-darwin-arm64" "15.1.0"
|
||||
"@next/swc-darwin-x64" "15.1.0"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.0"
|
||||
"@next/swc-linux-arm64-musl" "15.1.0"
|
||||
"@next/swc-linux-x64-gnu" "15.1.0"
|
||||
"@next/swc-linux-x64-musl" "15.1.0"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.0"
|
||||
"@next/swc-win32-x64-msvc" "15.1.0"
|
||||
sharp "^0.33.5"
|
||||
|
||||
nice-try@^1.0.4:
|
||||
@@ -6814,11 +6800,6 @@ read-pkg@^3.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^3.0.0"
|
||||
|
||||
readdirp@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.1.tgz#bd115327129672dc47f87408f05df9bd9ca3ef55"
|
||||
integrity sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
|
||||
@@ -8106,13 +8087,6 @@ unist-builder@^3.0.0:
|
||||
dependencies:
|
||||
"@types/unist" "^2.0.0"
|
||||
|
||||
unist-builder@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-4.0.0.tgz#817b326c015a6f9f5e92bb55b8e8bc5e578fe243"
|
||||
integrity sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==
|
||||
dependencies:
|
||||
"@types/unist" "^3.0.0"
|
||||
|
||||
unist-util-generated@^1.0.0:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz"
|
||||
|
||||
Reference in New Issue
Block a user