Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: upgrade to app router #7437

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_"}],
"react-hooks/exhaustive-deps": "error",
"react/no-unknown-property": ["error", {"ignore": ["meta"]}],
"react-compiler/react-compiler": "error"
"react-compiler/react-compiler": "error",
"@next/next/no-img-element": "off",
"@next/next/no-html-link-for-pages": "off"
},
"env": {
"node": true,
Expand Down
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const nextConfig = {
reactCompiler: true,
},
env: {},
serverExternalPackages: [],
webpack: (config, {dev, isServer, ...options}) => {
if (process.env.ANALYZE) {
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,21 @@
"@docsearch/react": "^3.6.1",
"@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.1.0",
"next": "^15.1.5",
"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"
"remark-gfm": "^3.0.1",
"unist-builder": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
Expand Down
131 changes: 131 additions & 0 deletions src/app/[[...markdownPath]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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 {generateRssFeed} from '../../utils/rss';
import {RouteItem} from 'components/Layout/getRouteMeta';

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}`);
}
}

// This replaces getStaticProps
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');
}

// Generate RSS feed during build time
if (process.env.NODE_ENV === 'production') {
await generateRssFeed();
}

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: any[]}>;
}) {
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);

// 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}
languages={null}>
{content}
</Page>
);
}
// Configure dynamic segments to be statically generated
export const dynamicParams = false;
6 changes: 5 additions & 1 deletion src/pages/500.js → src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
'use client';

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/

import {Page} from 'components/Layout/Page';
import {MDXComponents} from 'components/MDX/MDXComponents';
import sidebarLearn from '../sidebarLearn.json';
import {RouteItem} from 'components/Layout/getRouteMeta';

const {Intro, MaxWidth, p: P, a: A} = MDXComponents;

export default function NotFound() {
return (
<Page
section="unknown"
toc={[]}
routeTree={sidebarLearn}
routeTree={sidebarLearn as RouteItem}
meta={{title: 'Something Went Wrong'}}>
<MaxWidth>
<Intro>
Expand Down
90 changes: 90 additions & 0 deletions src/app/errors/[errorCode]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Page} from 'components/Layout/Page';
import sidebarLearn from '../../../sidebarLearn.json';
import type {RouteItem} from 'components/Layout/getRouteMeta';
import {generateMDX} from 'utils/generateMDX';
import fs from 'fs/promises';
import path from 'path';
import {ErrorDecoderProvider} from 'components/ErrorDecoderProvider';
import {notFound} from 'next/navigation';

let errorCodesCache: Record<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();

return Object.keys(errorCodes!).map((code) => ({
errorCode: code,
}));
}

async function getErrorPageContent(params: {errorCode?: string}) {
const errorCodes = await getErrorCodes();

const code = params?.errorCode;

if (!code || !errorCodes?.[code]) {
notFound();
}

const rootDir = path.join(process.cwd(), 'src/content/errors');
let mdxPath = params?.errorCode || 'index';
let mdx;

try {
mdx = await fs.readFile(path.join(rootDir, mdxPath + '.md'), 'utf8');
} catch {
mdx = await fs.readFile(path.join(rootDir, 'generic.md'), 'utf8');
}

const {content, toc, meta} = await generateMDX(mdx, mdxPath, {
code,
errorCodes,
});

return {
content,
toc,
meta,
errorCode: code,
errorMessage: errorCodes[code],
};
}

export default async function ErrorDecoderPage({
params,
}: {
params: Promise<{errorCode?: string}>;
}) {
const {content, errorMessage, errorCode} = await getErrorPageContent(
await params
);

return (
<ErrorDecoderProvider errorMessage={errorMessage} errorCode={errorCode}>
<Page
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;
Loading
Loading