diff --git a/packages/commons/next-seo/src/jsonld/types/breadcrumbs.ts b/packages/commons/next-seo/src/jsonld/types/breadcrumbs.ts index 9b5dc55158..131d6ff978 100644 --- a/packages/commons/next-seo/src/jsonld/types/breadcrumbs.ts +++ b/packages/commons/next-seo/src/jsonld/types/breadcrumbs.ts @@ -1,4 +1,4 @@ -import z from "zod"; +import { z } from "zod"; export const ListElementSchema = z.object({ "@type": z.literal("ListItem"), diff --git a/packages/ui/app/src/analytics/CustomerAnalytics.tsx b/packages/ui/app/src/analytics/CustomerAnalytics.tsx index 68143d2d9e..be9eff0b39 100644 --- a/packages/ui/app/src/analytics/CustomerAnalytics.tsx +++ b/packages/ui/app/src/analytics/CustomerAnalytics.tsx @@ -1,4 +1,3 @@ -import { GoogleAnalytics } from "@next/third-parties/google"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; import { isEqual } from "lodash-es"; @@ -6,12 +5,13 @@ import dynamic from "next/dynamic"; import Script from "next/script"; import { ReactElement, memo } from "react"; import { DOCS_ATOM, DOMAIN_ATOM } from "../atoms"; -import { GoogleTagManager } from "./GoogleTagManager"; import { Posthog } from "./PosthogContainer"; import { renderSegmentSnippet } from "./segment"; const IntercomScript = dynamic(() => import("./IntercomScript").then((mod) => mod.IntercomScript)); const FullstoryScript = dynamic(() => import("./FullstoryScript").then((mod) => mod.FullstoryScript)); +const GoogleAnalytics = dynamic(() => import("@next/third-parties/google").then((mod) => mod.GoogleAnalytics)); +const GoogleTagManager = dynamic(() => import("./GoogleTagManager").then((mod) => mod.GoogleTagManager)); const ANALYTICS_ATOM = selectAtom(DOCS_ATOM, (docs) => docs.analytics ?? {}, isEqual); const ANALYTICS_CONFIG_ATOM = selectAtom(DOCS_ATOM, (docs) => docs.analyticsConfig ?? {}, isEqual); diff --git a/packages/ui/app/src/analytics/posthog.ts b/packages/ui/app/src/analytics/posthog.ts index e458718ea2..b9eed6883c 100644 --- a/packages/ui/app/src/analytics/posthog.ts +++ b/packages/ui/app/src/analytics/posthog.ts @@ -1,6 +1,6 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; +import type { DocsV1Read } from "@fern-api/fdr-sdk"; import { Router } from "next/router"; -import posthog, { PostHog } from "posthog-js"; +import type { PostHog } from "posthog-js"; import { useEffect } from "react"; import { safeCall } from "./sentry"; @@ -24,9 +24,10 @@ function posthogHasCustomer(instance: PostHog): instance is PostHogWithCustomer } let IS_POSTHOG_INITIALIZED = false; -function safeAccessPosthog(run: () => void): void { +async function safeAccessPosthog(run: (posthog: PostHog) => void): Promise { if (IS_POSTHOG_INITIALIZED) { - safeCall(run); + const posthog = (await import("posthog-js")).default; + safeCall(() => run(posthog)); } } @@ -35,7 +36,7 @@ function safeAccessPosthog(run: () => void): void { * * @param run */ -function ifCustomer(run: (hog: PostHogWithCustomer) => void): void { +function ifCustomer(posthog: PostHog, run: (hog: PostHogWithCustomer) => void): void { safeCall(() => { if (IS_POSTHOG_INITIALIZED && posthogHasCustomer(posthog)) { run(posthog); @@ -43,10 +44,11 @@ function ifCustomer(run: (hog: PostHogWithCustomer) => void): void { }); } -export function initializePosthog(customerConfig?: DocsV1Read.PostHogConfig): void { +export async function initializePosthog(customerConfig?: DocsV1Read.PostHogConfig): Promise { const apiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY?.trim(); if (process.env.NODE_ENV === "production" && apiKey != null && apiKey.length > 0 && !IS_POSTHOG_INITIALIZED) { const posthogProxy = "/api/fern-docs/analytics/posthog"; + const posthog = (await import("posthog-js")).default; posthog.init(apiKey, { api_host: posthogProxy, @@ -75,30 +77,30 @@ export function initializePosthog(customerConfig?: DocsV1Read.PostHogConfig): vo } export function identifyUser(userId: string): void { - safeAccessPosthog(() => { + void safeAccessPosthog((posthog) => { posthog.identify(userId); - ifCustomer((posthog) => posthog.customer.identify(userId)); + ifCustomer(posthog, (posthog) => posthog.customer.identify(userId)); }); } export function registerPosthogProperties(properties: Record): void { - safeAccessPosthog(() => { + void safeAccessPosthog((posthog) => { posthog.register(properties); - ifCustomer((posthog) => posthog.customer.register(properties)); + ifCustomer(posthog, (posthog) => posthog.customer.register(properties)); }); } export function resetPosthog(): void { - safeAccessPosthog(() => { + void safeAccessPosthog((posthog) => { posthog.reset(); - ifCustomer((posthog) => posthog.customer.reset()); + ifCustomer(posthog, (posthog) => posthog.customer.reset()); }); } export function capturePosthogEvent(eventName: string, properties?: Record): void { - safeAccessPosthog(() => { + void safeAccessPosthog((posthog) => { posthog.capture(eventName, properties); - ifCustomer((posthog) => posthog.customer.capture(eventName, properties)); + ifCustomer(posthog, (posthog) => posthog.customer.capture(eventName, properties)); }); } @@ -115,7 +117,6 @@ const trackPageView = (url: string) => { export function useInitializePosthog(customerConfig?: DocsV1Read.PostHogConfig): void { useEffect(() => { safeCall(() => initializePosthog(customerConfig)); - Router.events.on("routeChangeComplete", trackPageView); return () => { Router.events.off("routeChangeComplete", trackPageView); diff --git a/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx b/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx index 23a416cf4d..561935b4ca 100644 --- a/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundEndpoint.tsx @@ -6,7 +6,6 @@ import { compact, mapValues, once } from "lodash-es"; import { FC, ReactElement, useCallback, useState } from "react"; import urljoin from "url-join"; import { useCallbackOne } from "use-memo-one"; -import { capturePosthogEvent } from "../analytics/posthog"; import { captureSentryError } from "../analytics/sentry"; import { PLAYGROUND_AUTH_STATE_ATOM, @@ -92,6 +91,7 @@ export const PlaygroundEndpoint: FC = ({ endpoint, type } setResponse(loading()); try { + const { capturePosthogEvent } = await import("../analytics/posthog"); capturePosthogEvent("api_playground_request_sent", { endpointId: endpoint.id, endpointName: endpoint.title, diff --git a/packages/ui/app/src/atoms/theme.ts b/packages/ui/app/src/atoms/theme.ts index fe06064ebf..4f7da4b2e6 100644 --- a/packages/ui/app/src/atoms/theme.ts +++ b/packages/ui/app/src/atoms/theme.ts @@ -2,7 +2,6 @@ import { ColorsConfig } from "@fern-ui/fdr-utils"; import { atom, useAtom, useAtomValue } from "jotai"; import { atomWithRefresh, selectAtom } from "jotai/utils"; import { isEqual } from "lodash-es"; -import { createElement, memo } from "react"; import { noop } from "ts-essentials"; import { useCallbackOne } from "use-memo-one"; import { z } from "zod"; @@ -167,49 +166,3 @@ export function useInitializeTheme(): void { }, []), ); } - -// this script cannot reference any other code since it will be stringified to be executed in the browser -export const script = (themes: AvailableThemes): void => { - const el = document.documentElement; - - function updateDOM(theme: string) { - el.classList.remove("light", "dark"); - el.classList.add(theme); - el.style.colorScheme = theme; - } - - function getSystemTheme() { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } - - if (themes.length === 1) { - updateDOM(themes[0]); - } else { - try { - const themeName = localStorage.getItem("theme") ?? "system"; - const isSystem = themes.length > 0 && themeName === "system"; - const theme = isSystem ? getSystemTheme() : themeName; - updateDOM(theme); - } catch { - // - } - } -}; - -// including the ThemeScript component will prevent a flash of unstyled content (FOUC) when the page loads -// by setting the theme from local storage before the page is rendered -export const ThemeScript = memo( - ({ nonce, colors }: { nonce?: string; colors?: ColorsConfig }) => { - const scriptArgs = JSON.stringify(getAvailableThemes(colors)); - - return createElement("script", { - suppressHydrationWarning: true, - nonce: typeof window === "undefined" ? nonce : "", - dangerouslySetInnerHTML: { __html: `(${script.toString()})(${scriptArgs})` }, - }); - }, - (prev, next) => - prev.nonce === next.nonce && getAvailableThemes(prev.colors).length === getAvailableThemes(next.colors).length, -); - -ThemeScript.displayName = "ThemeScript"; diff --git a/packages/ui/app/src/mdx/components/index.tsx b/packages/ui/app/src/mdx/components/index.tsx index e06e657b4d..d2f6a0df55 100644 --- a/packages/ui/app/src/mdx/components/index.tsx +++ b/packages/ui/app/src/mdx/components/index.tsx @@ -1,5 +1,6 @@ import { RemoteFontAwesomeIcon } from "@fern-ui/components"; import type { MDXComponents } from "mdx/types"; +import dynamic from "next/dynamic"; import { ComponentProps, PropsWithChildren, ReactElement } from "react"; import { FernErrorBoundaryProps, FernErrorTag } from "../../components/FernErrorBoundary"; import { AccordionGroup } from "./accordion"; @@ -20,7 +21,6 @@ import { } from "./callout"; import { Card, CardGroup } from "./card"; import { ClientLibraries } from "./client-libraries"; -import { CodeBlock, CodeGroup } from "./code"; import { Column, ColumnGroup } from "./columns"; import { Frame } from "./frame"; import { A, HeadingRenderer, Image, Li, Ol, Strong, Ul } from "./html"; @@ -33,6 +33,9 @@ import { Steps } from "./steps"; import { TabGroup } from "./tabs"; import { Tooltip } from "./tooltip"; +const CodeBlock = dynamic(() => import("./code").then((mod) => mod.CodeBlock)); +const CodeGroup = dynamic(() => import("./code").then((mod) => mod.CodeGroup)); + const FERN_COMPONENTS = { AccordionGroup, Availability, diff --git a/packages/ui/app/src/mdx/plugins/rehypeFernCode.ts b/packages/ui/app/src/mdx/plugins/rehypeFernCode.ts index cfc693f3b4..2d04c66aed 100644 --- a/packages/ui/app/src/mdx/plugins/rehypeFernCode.ts +++ b/packages/ui/app/src/mdx/plugins/rehypeFernCode.ts @@ -2,7 +2,7 @@ import type { Element, Root } from "hast"; import type { MdxJsxAttribute, MdxJsxFlowElementHast } from "mdast-util-mdx-jsx"; import rangeParser from "parse-numeric-range"; import { visit } from "unist-util-visit"; -import { FernSyntaxHighlighterProps } from "../../syntax-highlighting/FernSyntaxHighlighter"; +import type { FernSyntaxHighlighterProps } from "../../syntax-highlighting/FernSyntaxHighlighter"; import { unknownToString } from "../../util/unknownToString"; import type { CodeGroup } from "../components/code"; import { isElement, isMdxJsxFlowElement, isText, toAttribute } from "./utils"; diff --git a/packages/ui/app/src/next-app/DocsPage.tsx b/packages/ui/app/src/next-app/DocsPage.tsx index 2fc17386f7..b0a79c79b2 100644 --- a/packages/ui/app/src/next-app/DocsPage.tsx +++ b/packages/ui/app/src/next-app/DocsPage.tsx @@ -10,7 +10,6 @@ import { useRouteChanged } from "../hooks/useRouteChanged"; import { NextSeo } from "../seo/NextSeo"; import { InitializeTheme } from "../themes"; import { ThemedDocs } from "../themes/ThemedDocs"; -import { scrollToRoute } from "../util/anchor"; import { JavascriptProvider } from "./utils/JavascriptProvider"; const SearchDialog = dynamic(() => import("../search/SearchDialog").then(({ SearchDialog }) => SearchDialog), { @@ -26,7 +25,8 @@ export function DocsPage(pageProps: DocsProps): ReactElement | null { // this is a hack to scroll to the correct anchor position when the route changes (see workato's docs) // the underlying issue is that content rendering is delayed by an undetermined amount of time, so the anchor doesn't exist yet. // TODO: fix this properly. - useRouteChanged((route) => { + useRouteChanged(async (route) => { + const scrollToRoute = await import("../util/anchor").then((mod) => mod.scrollToRoute); const scroll = () => scrollToRoute(route); if (!scroll()) { setTimeout(scroll, 150); diff --git a/packages/ui/app/src/next-app/NextApp.tsx b/packages/ui/app/src/next-app/NextApp.tsx index 1bc66bc973..70dba8e31d 100644 --- a/packages/ui/app/src/next-app/NextApp.tsx +++ b/packages/ui/app/src/next-app/NextApp.tsx @@ -6,10 +6,11 @@ import PageLoader from "next/dist/client/page-loader"; import { Router } from "next/router"; import { ReactElement, useEffect } from "react"; import { SWRConfig } from "swr"; -import { DocsProps, ThemeScript, store } from "../atoms"; +import { DocsProps, store } from "../atoms"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; import "../css/globals.scss"; import { NextNProgress } from "../docs/NProgress"; +import { ThemeScript } from "./utils/ThemeScript"; export function NextApp({ Component, pageProps, router }: AppProps): ReactElement { // This is a hack to handle edge-cases related to multitenant subpath rendering: diff --git a/packages/ui/app/src/next-app/utils/ThemeScript.tsx b/packages/ui/app/src/next-app/utils/ThemeScript.tsx new file mode 100644 index 0000000000..4249ed6ca1 --- /dev/null +++ b/packages/ui/app/src/next-app/utils/ThemeScript.tsx @@ -0,0 +1,50 @@ +import type { ColorsConfig } from "@fern-ui/fdr-utils"; +import Script from "next/script"; +import type { ReactElement } from "react"; +import type { AvailableThemes } from "../../atoms"; + +// this script cannot reference any other code since it will be stringified to be executed in the browser +const script = (themes: AvailableThemes): void => { + const el = document.documentElement; + + function updateDOM(theme: string) { + el.classList.remove("light", "dark"); + el.classList.add(theme); + el.style.colorScheme = theme; + } + + function getSystemTheme() { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + + if (themes.length === 1) { + updateDOM(themes[0]); + } else { + try { + const themeName = localStorage.getItem("theme") ?? "system"; + const isSystem = themes.length > 0 && themeName === "system"; + const theme = isSystem ? getSystemTheme() : themeName; + updateDOM(theme); + } catch { + // + } + } +}; + +const getAvailableThemes = (colors: Partial = {}): AvailableThemes => { + if (Boolean(colors.dark) === Boolean(colors.light)) { + return ["light", "dark"]; + } + + return colors.dark ? ["dark"] : ["light"]; +}; + +export function ThemeScript({ colors }: { colors?: ColorsConfig }): ReactElement { + const args = getAvailableThemes(colors); + return ( +