feat(nx-dev): Use app router for blogs (#23127)
The PR activates the app router for the Blog page at /blog. Its purpose is to test Next.js changes within nx-dev, allowing us to identify and address any issues that users might encounter. Integrating these changes into our environment, we can gain firsthand experience and insights into potential problems, ensuring that the updates are robust and reliable. This approach helps us improve the overall quality and user experience of our platform by proactively identifying and resolving any issues that could affect consumers.
This commit is contained in:
parent
8351cb11f3
commit
b9b89b2575
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
/* nx-ignore-next-line */
|
||||
import type {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
/* eslint-disable @nx/enforce-module-boundaries */
|
||||
/* nx-ignore-next-line */
|
||||
import type { ProjectGraphProjectNode } from 'nx/src/config/project-graph';
|
||||
|
||||
@ -3,6 +3,7 @@ import { join, basename } from 'path';
|
||||
import { extractFrontmatter } from '@nx/nx-dev/ui-markdoc';
|
||||
import { sortPosts } from './blog.util';
|
||||
import { BlogPostDataEntry } from './blog.model';
|
||||
import { readFile, readdir } from 'fs/promises';
|
||||
|
||||
export class BlogApi {
|
||||
constructor(
|
||||
@ -19,6 +20,43 @@ export class BlogApi {
|
||||
}
|
||||
}
|
||||
|
||||
async getBlogs(): Promise<BlogPostDataEntry[]> {
|
||||
const files: string[] = await readdir(this.options.blogRoot);
|
||||
const authors = JSON.parse(
|
||||
readFileSync(join(this.options.blogRoot, 'authors.json'), 'utf8')
|
||||
);
|
||||
const allPosts: BlogPostDataEntry[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = join(this.options.blogRoot, file);
|
||||
if (!filePath.endsWith('.md')) continue;
|
||||
const content = await readFile(filePath, 'utf8');
|
||||
const frontmatter = extractFrontmatter(content);
|
||||
const slug = this.calculateSlug(filePath, frontmatter);
|
||||
const post = {
|
||||
content,
|
||||
title: frontmatter.title ?? null,
|
||||
description: frontmatter.description ?? null,
|
||||
authors: authors.filter((author) =>
|
||||
frontmatter.authors.includes(author.name)
|
||||
),
|
||||
date: this.calculateDate(file, frontmatter),
|
||||
cover_image: frontmatter.cover_image
|
||||
? `/documentation${frontmatter.cover_image}` // Match the prefix used by markdown parser
|
||||
: null,
|
||||
tags: frontmatter.tags ?? [],
|
||||
reposts: frontmatter.reposts ?? [],
|
||||
pinned: frontmatter.pinned ?? false,
|
||||
filePath,
|
||||
slug,
|
||||
};
|
||||
if (!frontmatter.draft || process.env.NODE_ENV === 'development') {
|
||||
allPosts.push(post);
|
||||
}
|
||||
}
|
||||
return sortPosts(allPosts);
|
||||
}
|
||||
|
||||
getBlogPosts(): BlogPostDataEntry[] {
|
||||
const files: string[] = readdirSync(this.options.blogRoot);
|
||||
const authors = JSON.parse(
|
||||
@ -68,6 +106,13 @@ export class BlogApi {
|
||||
}
|
||||
return blog;
|
||||
}
|
||||
// Optimize this so we don't read the FS multiple times
|
||||
async getBlogPostBySlug(
|
||||
slug: string | null
|
||||
): Promise<BlogPostDataEntry | undefined> {
|
||||
if (!slug) throw new Error(`Could not find blog post with slug: ${slug}`);
|
||||
return (await this.getBlogs()).find((post) => post.slug === slug);
|
||||
}
|
||||
|
||||
private calculateSlug(filePath: string, frontmatter: any): string {
|
||||
const baseName = basename(filePath, '.md');
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/solid';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
|
||||
18
nx-dev/nx-dev/app/app-router-analytics.tsx
Normal file
18
nx-dev/nx-dev/app/app-router-analytics.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
import { sendPageViewEvent } from '@nx/nx-dev/feature-analytics';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function AppRouterAnalytics({ gaMeasurementId }) {
|
||||
const pathName = usePathname();
|
||||
const [lastPath, setLastPath] = useState(pathName);
|
||||
|
||||
useEffect(() => {
|
||||
if (pathName !== lastPath) {
|
||||
setLastPath(pathName);
|
||||
sendPageViewEvent({ gaId: gaMeasurementId, path: pathName });
|
||||
}
|
||||
}, [pathName, gaMeasurementId, lastPath]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
54
nx-dev/nx-dev/app/blog/[slug]/page.tsx
Normal file
54
nx-dev/nx-dev/app/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import type { Metadata, ResolvingMetadata } from 'next';
|
||||
import { blogApi } from '../../../lib/blog.api';
|
||||
import { BlogDetails } from '@nx/nx-dev/ui-blog';
|
||||
interface BlogPostDetailProps {
|
||||
params: { slug: string };
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params: { slug } }: BlogPostDetailProps,
|
||||
parent: ResolvingMetadata
|
||||
): Promise<Metadata> {
|
||||
const post = await blogApi.getBlogPostBySlug(slug);
|
||||
const previousImages = (await parent).openGraph?.images ?? [];
|
||||
return {
|
||||
title: `${post.title} | Nx Blog`,
|
||||
description: 'Latest news from the Nx & Nx Cloud core team',
|
||||
openGraph: {
|
||||
url: `https://nx.dev/blog/${slug}`,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
images: [
|
||||
{
|
||||
url: post.cover_image
|
||||
? `https://nx.dev${post.cover_image}`
|
||||
: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart, Fast and Extensible Build System',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
...previousImages,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return (await blogApi.getBlogs()).map((post) => {
|
||||
return { slug: post.slug };
|
||||
});
|
||||
}
|
||||
|
||||
export default async function BlogPostDetail({
|
||||
params: { slug },
|
||||
}: BlogPostDetailProps) {
|
||||
const blog = await blogApi.getBlogPostBySlug(slug);
|
||||
return blog ? (
|
||||
<>
|
||||
{/* This empty div is necessary as app router does not automatically scroll on route changes */}
|
||||
<div></div>
|
||||
<BlogDetails post={blog} />
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
33
nx-dev/nx-dev/app/blog/page.tsx
Normal file
33
nx-dev/nx-dev/app/blog/page.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { blogApi } from '../../lib/blog.api';
|
||||
import { BlogContainer } from '@nx/nx-dev/ui-blog';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
|
||||
description: 'Latest news from the Nx & Nx Cloud core team',
|
||||
openGraph: {
|
||||
url: 'https://nx.dev/blog',
|
||||
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
|
||||
description:
|
||||
'Stay updated with the latest news, articles, and updates from the Nx & Nx Cloud team.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart Monorepos · Fast CI',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
siteName: 'NxDev',
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
async function getBlogs() {
|
||||
return await blogApi.getBlogPosts();
|
||||
}
|
||||
|
||||
export default async function BlogIndex() {
|
||||
const blogs = await getBlogs();
|
||||
return <BlogContainer blogPosts={blogs} />;
|
||||
}
|
||||
77
nx-dev/nx-dev/app/global-scripts.tsx
Normal file
77
nx-dev/nx-dev/app/global-scripts.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import Script from 'next/script';
|
||||
|
||||
export default function GlobalScripts({ gaMeasurementId }) {
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="gtag-script-dependency"
|
||||
strategy="afterInteractive"
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=${gaMeasurementId}`}
|
||||
/>
|
||||
<Script
|
||||
id="gtag-script-loader"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){ dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaMeasurementId}', {
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{/* Apollo.io Embed Code */}
|
||||
<Script
|
||||
type="text/javascript"
|
||||
id="apollo-script-loader"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `function initApollo(){var n=Math.random().toString(36).substring(7),o=document.createElement("script"); o.src="https://assets.apollo.io/micro/website-tracker/tracker.iife.js?nocache="+n,o.async=!0,o.defer=!0,o.onload=function(){window.trackingFunctions.onLoad({appId:"65e1db2f1976f30300fd8b26"})},document.head.appendChild(o)}initApollo();`,
|
||||
}}
|
||||
/>
|
||||
{/* HubSpot Analytics */}
|
||||
<Script
|
||||
id="hs-script-loader"
|
||||
strategy="afterInteractive"
|
||||
src="https://js.hs-scripts.com/2757427.js"
|
||||
/>
|
||||
{/* HubSpot FORMS Embed Code */}
|
||||
<Script
|
||||
type="text/javascript"
|
||||
id="hs-forms-script-loader"
|
||||
strategy="afterInteractive"
|
||||
src="//js.hsforms.net/forms/v2.js"
|
||||
/>
|
||||
{/* Hotjar Analytics */}
|
||||
<Script
|
||||
id="hotjar-script-loader"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(h,o,t,j,a,r){
|
||||
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
|
||||
h._hjSettings={hjid:2774127,hjsv:6};
|
||||
a=o.getElementsByTagName('head')[0];
|
||||
r=o.createElement('script');r.async=1;
|
||||
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
|
||||
a.appendChild(r);
|
||||
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,
|
||||
}}
|
||||
/>
|
||||
<Script
|
||||
id="twitter-campain-pixelcode"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
|
||||
},s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
|
||||
a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
|
||||
twq('config','obtp4');
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
91
nx-dev/nx-dev/app/layout.tsx
Normal file
91
nx-dev/nx-dev/app/layout.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Header, Footer, AnnouncementBanner } from '@nx/nx-dev/ui-common';
|
||||
import AppRouterAnalytics from './app-router-analytics';
|
||||
import GlobalScripts from './global-scripts';
|
||||
|
||||
import '../styles/main.css';
|
||||
|
||||
// Metadata for the entire site
|
||||
export const metadata: Metadata = {
|
||||
appleWebApp: { title: 'Nx' },
|
||||
applicationName: 'Nx',
|
||||
icons: [
|
||||
{
|
||||
url: '/favicon/favicon.svg',
|
||||
type: 'image/svg+xml',
|
||||
rel: 'icon',
|
||||
},
|
||||
{
|
||||
url: '/favicon/favicon-32x32.png',
|
||||
sizes: '32x32',
|
||||
type: 'image/png',
|
||||
rel: 'icon',
|
||||
},
|
||||
{
|
||||
url: '/favicon/favicon.ico',
|
||||
type: 'image/x-icon',
|
||||
rel: 'icon',
|
||||
},
|
||||
{
|
||||
url: '/favicon/apple-touch-icon.png',
|
||||
sizes: '180x180',
|
||||
rel: 'apple-touch-icon',
|
||||
},
|
||||
{
|
||||
url: '/favicon/safari-pinned-tab.svg',
|
||||
color: '#5bbad5',
|
||||
rel: 'mask-icon',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Viewport settings for the entire site
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: '#F8FAFC' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#0F172A' },
|
||||
],
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const gaMeasurementId = 'UA-88380372-10';
|
||||
return (
|
||||
<html lang="en" className="h-full scroll-smooth" suppressHydrationWarning>
|
||||
<AppRouterAnalytics gaMeasurementId={gaMeasurementId} />
|
||||
<head>
|
||||
<meta
|
||||
name="msapplication-TileColor"
|
||||
content="#DA532C"
|
||||
key="windows-tile-color"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
try {
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
} catch (_) {}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="h-full bg-white text-slate-700 antialiased selection:bg-blue-500 selection:text-white dark:bg-slate-900 dark:text-slate-400 dark:selection:bg-sky-500">
|
||||
<AnnouncementBanner />
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<GlobalScripts gaMeasurementId={gaMeasurementId} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { DocumentsApi } from '@nx/nx-dev/data-access-documents/node-only';
|
||||
import documents from '../public/documentation/generated/manifests/recipes.json';
|
||||
import { tagsApi } from './tags.api';
|
||||
|
||||
export const nxRecipesApi = new DocumentsApi({
|
||||
id: 'recipes',
|
||||
manifest: documents,
|
||||
prefix: '',
|
||||
publicDocsRoot: 'public/documentation',
|
||||
tagsApi,
|
||||
});
|
||||
1
nx-dev/nx-dev/next-env.d.ts
vendored
1
nx-dev/nx-dev/next-env.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@ -3,6 +3,10 @@ const { withNx } = require('@nx/next/plugins/with-nx');
|
||||
const redirectRules = require('./redirect-rules');
|
||||
|
||||
module.exports = withNx({
|
||||
// Disable the type checking for now, we need to resolve the issues first.
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
// For both client and server
|
||||
env: {
|
||||
VERCEL: process.env.VERCEL,
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
import { GetStaticProps, GetStaticPaths } from 'next';
|
||||
import { blogApi } from '../../lib/blog.api';
|
||||
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { Footer, Header } from '@nx/nx-dev/ui-common';
|
||||
import { BlogDetails } from '@nx/nx-dev/ui-blog';
|
||||
|
||||
interface BlogPostDetailProps {
|
||||
post: BlogPostDataEntry;
|
||||
}
|
||||
|
||||
export default function BlogPostDetail({ post }: BlogPostDetailProps) {
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={`${post.title} | Nx Blog`}
|
||||
description="Latest news from the Nx & Nx Cloud core team"
|
||||
openGraph={{
|
||||
url: 'https://nx.dev', // + router.asPath,
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
images: [
|
||||
{
|
||||
url: post.cover_image
|
||||
? `https://nx.dev${post.cover_image}`
|
||||
: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart, Fast and Extensible Build System',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
siteName: 'NxDev',
|
||||
type: 'website',
|
||||
}}
|
||||
/>
|
||||
<Header />
|
||||
<BlogDetails post={post} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async (context) => {
|
||||
// optimize s.t. we don't read the FS multiple times; for now it's ok
|
||||
// const posts = await blogApi.getBlogPosts();
|
||||
// const post = posts.find((p) => p.slug === context.params?.slug);
|
||||
try {
|
||||
const post = await blogApi.getBlogPost(context.params?.slug as string);
|
||||
return { props: { post } };
|
||||
} catch (e) {
|
||||
return {
|
||||
notFound: true,
|
||||
props: {
|
||||
statusCode: 404,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const posts = await blogApi.getBlogPosts();
|
||||
|
||||
const paths = posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
}));
|
||||
|
||||
return { paths, fallback: 'blocking' };
|
||||
};
|
||||
@ -1,53 +0,0 @@
|
||||
import { Footer, Header } from '@nx/nx-dev/ui-common';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useRouter } from 'next/router';
|
||||
import { BlogPostDataEntry } from '@nx/nx-dev/data-access-documents/node-only';
|
||||
|
||||
import { blogApi } from '../../lib/blog.api';
|
||||
import { BlogContainer } from '@nx/nx-dev/ui-blog';
|
||||
|
||||
interface BlogListProps {
|
||||
blogposts: BlogPostDataEntry[];
|
||||
}
|
||||
|
||||
export function getStaticProps(): { props: BlogListProps } {
|
||||
const blogposts = blogApi.getBlogPosts();
|
||||
return {
|
||||
props: {
|
||||
blogposts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function Blog({ blogposts }: BlogListProps): JSX.Element {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title="Nx Blog - Updates from the Nx & Nx Cloud team"
|
||||
description="Latest news from the Nx & Nx Cloud core team"
|
||||
openGraph={{
|
||||
url: 'https://nx.dev' + router.asPath,
|
||||
title: 'Nx Blog - Updates from the Nx & Nx Cloud team',
|
||||
description:
|
||||
'Stay updated with the latest news, articles, and updates from the Nx & Nx Cloud team.',
|
||||
images: [
|
||||
{
|
||||
url: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart Monorepos · Fast CI',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
siteName: 'NxDev',
|
||||
type: 'website',
|
||||
}}
|
||||
/>
|
||||
<Header />
|
||||
<BlogContainer blogPosts={blogposts} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -70,7 +70,11 @@ export default function RspackConfigSetup({
|
||||
role="main"
|
||||
className="flex h-full flex-1 overflow-y-hidden"
|
||||
>
|
||||
<SidebarContainer menu={vm.menu} navIsOpen={navIsOpen} />
|
||||
<SidebarContainer
|
||||
menu={vm.menu}
|
||||
navIsOpen={navIsOpen}
|
||||
toggleNav={toggleNav}
|
||||
/>
|
||||
<div
|
||||
ref={wrapperElement}
|
||||
id="wrapper"
|
||||
|
||||
@ -31,7 +31,7 @@ module.exports = {
|
||||
mode: 'jit',
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
path.join(__dirname, 'pages/**/*.{js,ts,jsx,tsx}'),
|
||||
path.join(__dirname, '{pages,app}/**/*.{js,ts,jsx,tsx}'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node", "jest"],
|
||||
"lib": ["dom", "es2019"]
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.ts", "**/*.tsx", "next-env.d.ts"]
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"jsx": "preserve",
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
@ -10,18 +11,34 @@
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"strictNullChecks": false,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"types": ["node", "jest"],
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"../../dist/nx-dev/nx-dev/.next/types/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"jest.config.ts",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export { BlogAuthors } from './lib/authors';
|
||||
export { BlogEntry, BlogEntryProps } from './lib/blog-entry';
|
||||
export { MoreBlogs, MoreBlogsProps } from './lib/more-blogs';
|
||||
export { BlogDetails, BlogDetailsProps } from './lib/blog-details';
|
||||
export { BlogEntry } from './lib/blog-entry';
|
||||
export { MoreBlogs } from './lib/more-blogs';
|
||||
export { BlogDetails } from './lib/blog-details';
|
||||
export { BlogContainer } from './lib/blog-container';
|
||||
export { FeaturedBlogs } from './lib/featured-blogs';
|
||||
|
||||
@ -4,8 +4,10 @@ import type { BlogAuthor } from '@nx/nx-dev/data-access-documents/node-only';
|
||||
|
||||
export function BlogAuthors({
|
||||
authors,
|
||||
showAuthorDetails = true,
|
||||
}: {
|
||||
authors: BlogAuthor[];
|
||||
showAuthorDetails?: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="relative isolate flex items-center -space-x-2">
|
||||
@ -21,7 +23,7 @@ export function BlogAuthors({
|
||||
src={`/documentation/blog/images/authors/${author.name}.jpeg`}
|
||||
className="relative inline-block h-6 w-6 rounded-full ring-1 ring-white grayscale dark:ring-slate-900"
|
||||
/>
|
||||
<AuthorDetail author={author} />
|
||||
{showAuthorDetails && <AuthorDetail author={author} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -8,6 +8,27 @@ import { renderMarkdown } from '@nx/nx-dev/ui-markdoc';
|
||||
export interface BlogDetailsProps {
|
||||
post: BlogPostDataEntry;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ post }: BlogDetailsProps) {
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
openGraph: {
|
||||
images: [
|
||||
{
|
||||
url: post.cover_image
|
||||
? `https://nx.dev${post.cover_image}`
|
||||
: 'https://nx.dev/socials/nx-media.png',
|
||||
width: 800,
|
||||
height: 421,
|
||||
alt: 'Nx: Smart, Fast and Extensible Build System',
|
||||
type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function BlogDetails({ post }: BlogDetailsProps) {
|
||||
const { node } = renderMarkdown(post.content, {
|
||||
filePath: post.filePath ?? '',
|
||||
|
||||
@ -41,7 +41,7 @@ export function MoreBlogs({ blogs }: MoreBlogsProps) {
|
||||
{formattedDate}
|
||||
</span>
|
||||
<span className="hidden flex-1 overflow-hidden sm:inline-block">
|
||||
<BlogAuthors authors={post.authors} />
|
||||
<BlogAuthors authors={post.authors} showAuthorDetails={false} />
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@ -19,7 +19,6 @@ export * from './lib/tweet';
|
||||
export * from './lib/typography';
|
||||
export * from './lib/github-star-widget';
|
||||
export * from './lib/youtube.component';
|
||||
export * from './lib/image-theme';
|
||||
export * from './lib/twitter-icon';
|
||||
export * from './lib/discord-icon';
|
||||
export { resourceMenuItems } from './lib/headers/menu-items';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import cx from 'classnames';
|
||||
import { ReactNode, createContext, useState } from 'react';
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Fragment, type JSX } from 'react';
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Dialog, Disclosure, Popover, Transition } from '@headlessui/react';
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
@ -7,7 +8,6 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import cx from 'classnames';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { ButtonLink } from '../button';
|
||||
import {
|
||||
@ -28,7 +28,6 @@ import { NxCloudIcon } from '../nx-cloud-icon';
|
||||
|
||||
export function Header(): JSX.Element {
|
||||
let [isOpen, setIsOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
// We need to close the popover if the route changes or the window is resized to prevent the popover from being stuck open.
|
||||
const checkSizeAndClosePopover = () => {
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface ImageThemeProps {
|
||||
lightSrc: string;
|
||||
darkSrc: string;
|
||||
alt?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function ImageTheme({
|
||||
lightSrc,
|
||||
darkSrc,
|
||||
alt,
|
||||
...props
|
||||
}: ImageThemeProps) {
|
||||
const [src, setSrc] = useState(lightSrc);
|
||||
|
||||
// Listen for theme change and update the image
|
||||
useEffect(() => {
|
||||
const updateImageSource = () => {
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
setSrc(isDarkMode ? darkSrc : lightSrc);
|
||||
};
|
||||
|
||||
updateImageSource();
|
||||
|
||||
// Event listener for changes in system theme
|
||||
const themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
themeMediaQuery.addEventListener('change', updateImageSource);
|
||||
|
||||
const observer = new MutationObserver(updateImageSource);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
|
||||
// Cleanup function to remove event listener and observer on component unmount
|
||||
return () => {
|
||||
themeMediaQuery.removeEventListener('change', updateImageSource);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Image src={src} alt={alt ?? ''} {...props} />;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Menu, MenuItem, MenuSection } from '@nx/nx-dev/models-menu';
|
||||
import { Sidebar, SidebarMobile } from './sidebar';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { AlgoliaSearch } from '@nx/nx-dev/feature-search';
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export { Fence, FenceProps } from './lib/fence';
|
||||
export { Fence, type FenceProps } from './lib/fence';
|
||||
export { TerminalOutput } from './lib/fences/terminal-output';
|
||||
export { TerminalShellWrapper } from './lib/fences/terminal-shell';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import {
|
||||
ClipboardDocumentCheckIcon,
|
||||
ClipboardDocumentIcon,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
import { Fence, FenceProps } from '@nx/nx-dev/ui-fence';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useRouter, usePathname, useParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const useUrlHash = (initialValue: string) => {
|
||||
const router = useRouter();
|
||||
const [hash, setHash] = useState(initialValue);
|
||||
|
||||
const updateHash = (str: string) => {
|
||||
@ -11,34 +11,28 @@ const useUrlHash = (initialValue: string) => {
|
||||
setHash(str.split('#')[1]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onWindowHashChange = () => updateHash(window.location.hash);
|
||||
const onNextJSHashChange = (url: string) => updateHash(url);
|
||||
const params = useParams();
|
||||
|
||||
router.events.on('hashChangeStart', onNextJSHashChange);
|
||||
window.addEventListener('hashchange', onWindowHashChange);
|
||||
window.addEventListener('load', onWindowHashChange);
|
||||
return () => {
|
||||
router.events.off('hashChangeStart', onNextJSHashChange);
|
||||
window.removeEventListener('load', onWindowHashChange);
|
||||
window.removeEventListener('hashchange', onWindowHashChange);
|
||||
};
|
||||
}, [router.asPath, router.events]);
|
||||
useEffect(() => {
|
||||
updateHash(window.location.hash);
|
||||
}, [params]);
|
||||
|
||||
return hash;
|
||||
};
|
||||
|
||||
export function FenceWrapper(props: FenceProps) {
|
||||
const { push, asPath } = useRouter();
|
||||
const { push } = useRouter();
|
||||
const pathName = usePathname();
|
||||
const hash = decodeURIComponent(useUrlHash(''));
|
||||
|
||||
const modifiedProps: FenceProps = {
|
||||
...props,
|
||||
selectedLineGroup: hash,
|
||||
onLineGroupSelectionChange: (selection: string) => {
|
||||
push(asPath.split('#')[0] + '#' + selection);
|
||||
push(pathName.split('#')[0] + '#' + selection);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-8 w-full">
|
||||
<Fence {...modifiedProps} />
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ChevronDoubleUpIcon,
|
||||
ChevronDoubleDownIcon,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useTheme } from '@nx/nx-dev/ui-theme';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { JSX, ReactElement, useEffect, useState } from 'react';
|
||||
import { ProjectDetails as ProjectDetailsUi } from '@nx/graph/ui-project-details';
|
||||
import { ExpandedTargetsProvider } from '@nx/graph/shared';
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
// TODO@ben: refactor to use HeadlessUI tabs
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function VideoLoop({
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useLayoutEffect as ReactUseLayoutEffect } from 'react';
|
||||
|
||||
/**
|
||||
|
||||
@ -1,4 +1 @@
|
||||
export * from './lib/references-nav-list';
|
||||
export * from './lib/references-index-item';
|
||||
export * from './lib/references-section';
|
||||
export * from './lib/icons-map';
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import cx from 'classnames';
|
||||
import { iconsMap } from './icons-map';
|
||||
|
||||
export function ReferencesIndexItem(pkg: {
|
||||
active: string;
|
||||
callback: (id: any) => any;
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
onClick={() => pkg.callback(pkg.id)}
|
||||
className={cx(
|
||||
'group relative flex items-center justify-center gap-3 overflow-hidden rounded-lg border border-slate-200 px-3 py-2 text-slate-700 transition hover:bg-slate-50',
|
||||
pkg.active === pkg.id ? 'bg-slate-200' : ''
|
||||
)}
|
||||
>
|
||||
<img
|
||||
className="h-5 w-5 object-cover opacity-75"
|
||||
loading="lazy"
|
||||
src={iconsMap[pkg.id]}
|
||||
alt={pkg.name + ' illustration'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<span className="text-base font-medium">{pkg.name}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { MenuItem } from '@nx/nx-dev/models-menu';
|
||||
|
||||
export function ReferencesNavList({
|
||||
header,
|
||||
links,
|
||||
}: {
|
||||
header: {
|
||||
title: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
links: MenuItem;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<nav className="relative">
|
||||
<h4 className="mb-5 flex items-center text-lg font-bold tracking-tight text-slate-700 lg:text-xl">
|
||||
{header.icon}
|
||||
{header.title}
|
||||
</h4>
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{links.itemList?.map((item, subIndex) => (
|
||||
<li key={[item.id, subIndex].join('-')}>
|
||||
<a
|
||||
href={item.path}
|
||||
className="group block flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50/10 px-4 py-2 text-slate-500 transition-all hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
{item.name}
|
||||
<ChevronRightIcon className="h-4 w-4 transition-all group-hover:translate-x-2" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import { iconsMap } from './icons-map';
|
||||
|
||||
export function ReferencesPackageCard(pkg: {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="ml-3 block flex-shrink-0">
|
||||
<img
|
||||
className="h-10 w-10 rounded-lg object-cover opacity-75"
|
||||
loading="lazy"
|
||||
src={iconsMap[pkg.id]}
|
||||
alt={pkg.name + ' illustration'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-slate-900 lg:text-3xl">
|
||||
{pkg.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CogIcon,
|
||||
CommandLineIcon,
|
||||
CpuChipIcon,
|
||||
} from '@heroicons/react/24/solid';
|
||||
import { MenuItem } from '@nx/nx-dev/models-menu';
|
||||
import { ReferencesNavList } from './references-nav-list';
|
||||
import { ReferencesPackageCard } from './references-package-card';
|
||||
|
||||
export function ReferencesSection({
|
||||
section,
|
||||
}: {
|
||||
section: MenuItem;
|
||||
}): JSX.Element {
|
||||
const guides: MenuItem | null =
|
||||
section.itemList?.find((x) => x.id === section.id + '-guides') || null;
|
||||
const executors =
|
||||
section.itemList?.find((x) => x.id === section.id + '-executors') || null;
|
||||
const generators =
|
||||
section.itemList?.find((x) => x.id === section.id + '-generators') || null;
|
||||
|
||||
return (
|
||||
<section
|
||||
id={section.id}
|
||||
className="relative py-2 md:grid md:grid-cols-3 md:gap-x-16"
|
||||
>
|
||||
<header className="md:col-span-1">
|
||||
<ReferencesPackageCard
|
||||
id={section.id}
|
||||
name={section.name as string}
|
||||
path={section.path as string}
|
||||
/>
|
||||
</header>
|
||||
<div className="mt-10 grid grid-cols-2 gap-8 sm:grid-cols-3 sm:space-y-0 md:col-span-2 md:mt-0">
|
||||
{!!guides && (
|
||||
<ReferencesNavList
|
||||
header={{
|
||||
icon:
|
||||
section.id === 'nx' ? (
|
||||
<CommandLineIcon
|
||||
className="mr-2 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<BookmarkIcon className="mr-2 h-5 w-5" aria-hidden="true" />
|
||||
),
|
||||
title: section.id === 'nx' ? 'Commands' : 'Guides',
|
||||
}}
|
||||
links={guides}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!executors && (
|
||||
<ReferencesNavList
|
||||
header={{
|
||||
icon: <CpuChipIcon className="mr-2 h-5 w-5" aria-hidden="true" />,
|
||||
title: executors.name,
|
||||
}}
|
||||
links={executors}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!generators && (
|
||||
<ReferencesNavList
|
||||
header={{
|
||||
icon: <CogIcon className="mr-2 h-5 w-5" aria-hidden="true" />,
|
||||
title: generators.name,
|
||||
}}
|
||||
links={generators}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
import {
|
||||
ComputerDesktopIcon,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
'use client';
|
||||
import { useMemo, useSyncExternalStore } from 'react';
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'system';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user