From 2a23f1f00365268024234041157f480b8b5a18f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Wed, 23 Apr 2025 09:31:22 +0200 Subject: [PATCH 001/127] Improve performances of TOC (#3176) --- .../SiteSections/SiteSectionList.tsx | 52 ++-- .../TableOfContents/PageDocumentItem.tsx | 4 +- .../TableOfContents/PageGroupItem.tsx | 20 +- .../TableOfContents/PageLinkItem.tsx | 6 +- .../components/TableOfContents/PagesList.tsx | 55 ++-- .../TableOfContents/TOCScroller.tsx | 122 ++++---- .../TableOfContents/TableOfContents.tsx | 25 +- .../TableOfContents/ToggleableLinkItem.tsx | 294 +++++++++++------- .../components/hooks/useCurrentPagePath.ts | 11 +- .../components/hooks/useToggleAnimation.ts | 41 +-- 10 files changed, 344 insertions(+), 286 deletions(-) diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx index a8b83cfb5a..6026296839 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx @@ -75,13 +75,13 @@ export function SiteSectionListItem(props: { const isMounted = useIsMounted(); React.useEffect(() => {}, [isMounted]); // This updates the useScrollToActiveTOCItem hook once we're mounted, so we can actually scroll to the this item - const linkRef = React.createRef<HTMLAnchorElement>(); - useScrollToActiveTOCItem({ linkRef, isActive }); + const anchorRef = React.createRef<HTMLAnchorElement>(); + useScrollToActiveTOCItem({ anchorRef, isActive }); return ( <Link + ref={anchorRef} href={section.url} - ref={linkRef} aria-current={isActive && 'page'} className={tcls( 'group/section-link flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint', @@ -121,18 +121,15 @@ export function SiteSectionGroupItem(props: { const hasDescendants = group.sections.length > 0; const isActiveGroup = group.sections.some((section) => section.id === currentSection.id); - const [isVisible, setIsVisible] = React.useState(isActiveGroup); + const shouldOpen = hasDescendants && isActiveGroup; + const [isOpen, setIsOpen] = React.useState(shouldOpen); - // Update the visibility of the children, if we are navigating to a descendant. + // Update the visibility of the children if the group becomes active. React.useEffect(() => { - if (!hasDescendants) { - return; + if (shouldOpen) { + setIsOpen(shouldOpen); } - - setIsVisible((prev) => prev || isActiveGroup); - }, [isActiveGroup, hasDescendants]); - - const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible }); + }, [shouldOpen]); return ( <> @@ -141,7 +138,7 @@ export function SiteSectionGroupItem(props: { onClick={(event) => { event.preventDefault(); event.stopPropagation(); - setIsVisible((prev) => !prev); + setIsOpen((prev) => !prev); }} className={`group/section-link flex w-full flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 text-left transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint ${ isActiveGroup @@ -184,7 +181,7 @@ export function SiteSectionGroupItem(props: { 'after:h-7', 'hover:bg-tint-active', 'hover:text-current', - isActiveGroup ? ['hover:bg-tint-hover'] : [] + isActiveGroup && 'hover:bg-tint-hover' )} > <Icon @@ -201,17 +198,13 @@ export function SiteSectionGroupItem(props: { 'group-hover:opacity-11', 'contrast-more:opacity-11', - isVisible ? ['rotate-90'] : ['rotate-0'] + isOpen ? 'rotate-90' : 'rotate-0' )} /> </span> </button> {hasDescendants ? ( - <motion.div - ref={scope} - className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')} - initial={isVisible ? show : hide} - > + <Descendants isVisible={isOpen}> {group.sections.map((section) => ( <SiteSectionListItem section={section} @@ -220,8 +213,25 @@ export function SiteSectionGroupItem(props: { className="pl-5" /> ))} - </motion.div> + </Descendants> ) : null} </> ); } + +function Descendants(props: { + isVisible: boolean; + children: React.ReactNode; +}) { + const { isVisible, children } = props; + const { show, hide, scope } = useToggleAnimation(isVisible); + return ( + <motion.div + ref={scope} + className={isVisible ? undefined : '[&_ul>li]:opacity-1'} + initial={isVisible ? show : hide} + > + {children} + </motion.div> + ); +} diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx index ad7fc513e3..2ff3289d7f 100644 --- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx @@ -20,7 +20,7 @@ export async function PageDocumentItem(props: { const href = context.linker.toPathForPage({ pages: rootPages, page }); return ( - <li className={tcls('flex', 'flex-col')}> + <li className="flex flex-col"> <ToggleableLinkItem href={href} pathname={getPagePath(rootPages, page)} @@ -52,7 +52,7 @@ export async function PageDocumentItem(props: { } > {page.emoji || page.icon ? ( - <span className={tcls('flex', 'gap-3', 'items-center')}> + <span className="flex items-center gap-3"> <TOCPageIcon page={page} /> {page.title} </span> diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx index 884f6eedac..24ea366a2e 100644 --- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx @@ -15,27 +15,13 @@ export function PageGroupItem(props: { const { rootPages, page, context } = props; return ( - <li className={tcls('flex', 'flex-col', 'group/page-group-item')}> + <li className="group/page-group-item flex flex-col"> <div className={tcls( - 'flex', - 'items-center', - - 'gap-3', - 'px-3', - 'z-[1]', - 'sticky', - '-top-5', - 'pt-6', - 'group-first/page-group-item:-mt-5', + '-top-5 group-first/page-group-item:-mt-5 sticky z-[1] flex items-center gap-3 px-3 pt-6', + 'font-semibold text-xs uppercase tracking-wide', 'pb-3', // Add extra padding to make the header fade a bit nicer '-mb-1.5', // Then pull the page items a bit closer, effective bottom padding is 1.5 units / 6px. - - 'text-xs', - 'tracking-wide', - 'font-semibold', - 'uppercase', - '[mask-image:linear-gradient(rgba(0,0,0,1)_70%,rgba(0,0,0,0))]', // Fade out effect of fixed page items. We want the fade to start past the header, this is a good approximation. 'bg-tint-base', 'sidebar-filled:bg-tint-subtle', diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index 43c31730c5..636cc514d9 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -55,9 +55,9 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git 'shrink-0', 'text-current', 'transition-colors', - '[&>path]:transition-[opacity]', - '[&>path]:[opacity:0.40]', - 'group-hover:[&>path]:[opacity:1]' + '[&>path]:transition-opacity', + '[&>path]:opacity-[0.4]', + 'group-hover:[&>path]:opacity-11' )} /> </Link> diff --git a/packages/gitbook/src/components/TableOfContents/PagesList.tsx b/packages/gitbook/src/components/TableOfContents/PagesList.tsx index ff92f269b7..ccd7579ec3 100644 --- a/packages/gitbook/src/components/TableOfContents/PagesList.tsx +++ b/packages/gitbook/src/components/TableOfContents/PagesList.tsx @@ -1,8 +1,9 @@ -import { type RevisionPage, RevisionPageType } from '@gitbook/api'; +import type { RevisionPage } from '@gitbook/api'; import type { GitBookSiteContext } from '@v2/lib/context'; import { type ClassValue, tcls } from '@/lib/tailwind'; +import assertNever from 'assert-never'; import { PageDocumentItem } from './PageDocumentItem'; import { PageGroupItem } from './PageGroupItem'; import { PageLinkItem } from './PageLinkItem'; @@ -16,41 +17,45 @@ export function PagesList(props: { const { rootPages, pages, context, style } = props; return ( - <ul className={tcls('flex', 'flex-col', 'gap-y-0.5', style)}> + <ul className={tcls('flex flex-col gap-y-0.5', style)}> {pages.map((page) => { - if (page.type === RevisionPageType.Computed) { + if (page.type === 'computed') { throw new Error( 'Unexpected computed page, it should have been computed in the API' ); } - if (page.type === RevisionPageType.Link) { - return <PageLinkItem key={page.id} page={page} context={context} />; - } - if (page.hidden) { return null; } - if (page.type === RevisionPageType.Group) { - return ( - <PageGroupItem - key={page.id} - rootPages={rootPages} - page={page} - context={context} - /> - ); + switch (page.type) { + case 'document': + return ( + <PageDocumentItem + key={page.id} + rootPages={rootPages} + page={page} + context={context} + /> + ); + + case 'link': + return <PageLinkItem key={page.id} page={page} context={context} />; + + case 'group': + return ( + <PageGroupItem + key={page.id} + rootPages={rootPages} + page={page} + context={context} + /> + ); + + default: + assertNever(page); } - - return ( - <PageDocumentItem - key={page.id} - rootPages={rootPages} - page={page} - context={context} - /> - ); })} </ul> ); diff --git a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx index 9e9332ea1c..492dc9022d 100644 --- a/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx +++ b/packages/gitbook/src/components/TableOfContents/TOCScroller.tsx @@ -1,40 +1,64 @@ 'use client'; -import React from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + type ComponentPropsWithoutRef, +} from 'react'; +import { assert } from 'ts-essentials'; -import { type ClassValue, tcls } from '@/lib/tailwind'; +interface TOCScrollContainerContextType { + onContainerMount: (listener: (element: HTMLDivElement) => void) => () => void; +} -const TOCScrollContainerRefContext = React.createContext<React.RefObject<HTMLDivElement> | null>( - null -); +const TOCScrollContainerContext = React.createContext<TOCScrollContainerContextType | null>(null); -function useTOCScrollContainerRefContext() { - const ctx = React.useContext(TOCScrollContainerRefContext); - if (!ctx) { - throw new Error('Context `TOCScrollContainerRefContext` must be used within Provider'); - } +function useTOCScrollContainerContext() { + const ctx = React.useContext(TOCScrollContainerContext); + assert(ctx); return ctx; } -export function TOCScrollContainer(props: { - children: React.ReactNode; - className?: ClassValue; - style?: React.CSSProperties; -}) { - const { children, className, style } = props; - const scrollContainerRef = React.createRef<HTMLDivElement>(); +/** + * Table of contents scroll container. + */ +export function TOCScrollContainer(props: ComponentPropsWithoutRef<'div'>) { + const ref = useRef<HTMLDivElement>(null); + const listeners = useRef<((element: HTMLDivElement) => void)[]>([]); + const onContainerMount: TOCScrollContainerContextType['onContainerMount'] = useCallback( + (listener) => { + if (ref.current) { + listener(ref.current); + return () => {}; + } + listeners.current.push(listener); + return () => { + listeners.current = listeners.current.filter((l) => l !== listener); + }; + }, + [] + ); + const value: TOCScrollContainerContextType = useMemo( + () => ({ onContainerMount }), + [onContainerMount] + ); + useEffect(() => { + const element = ref.current; + if (!element) { + return; + } + listeners.current.forEach((listener) => listener(element)); + return () => { + listeners.current = []; + }; + }, []); return ( - <TOCScrollContainerRefContext.Provider value={scrollContainerRef}> - <div - ref={scrollContainerRef} - data-testid="toc-scroll-container" - className={tcls(className)} - style={style} - > - {children} - </div> - </TOCScrollContainerRefContext.Provider> + <TOCScrollContainerContext.Provider value={value}> + <div ref={ref} data-testid="toc-scroll-container" {...props} /> + </TOCScrollContainerContext.Provider> ); } @@ -42,39 +66,31 @@ export function TOCScrollContainer(props: { const TOC_ITEM_OFFSET = 100; /** - * Scrolls the table of contents container to the page item when it becomes active + * Scrolls the table of contents container to the page item when it's initially active. */ export function useScrollToActiveTOCItem(props: { + anchorRef: React.RefObject<HTMLAnchorElement>; isActive: boolean; - linkRef: React.RefObject<HTMLAnchorElement>; }) { - const { isActive, linkRef } = props; - const scrollContainerRef = useTOCScrollContainerRefContext(); - const isScrolled = React.useRef(false); - React.useLayoutEffect(() => { - if (!isActive) { - isScrolled.current = false; - return; - } - if (isScrolled.current) { - return; - } - const tocItem = linkRef.current; - const tocContainer = scrollContainerRef.current; - if (!tocItem || !tocContainer || !isOutOfView(tocItem, tocContainer)) { - return; + const { isActive, anchorRef } = props; + const isInitialActiveRef = useRef(isActive); + const { onContainerMount } = useTOCScrollContainerContext(); + useEffect(() => { + const anchor = anchorRef.current; + if (isInitialActiveRef.current && anchor) { + return onContainerMount((container) => { + if (isOutOfView(anchor, container)) { + container.scrollTo({ top: anchor.offsetTop - TOC_ITEM_OFFSET }); + } + }); } - tocContainer?.scrollTo({ - top: tocItem.offsetTop - TOC_ITEM_OFFSET, - }); - isScrolled.current = true; - }, [isActive, linkRef, scrollContainerRef]); + }, [onContainerMount, anchorRef]); } -function isOutOfView(tocItem: HTMLElement, tocContainer: HTMLElement) { - const tocItemTop = tocItem.offsetTop; - const containerTop = tocContainer.scrollTop; - const containerBottom = containerTop + tocContainer.clientHeight; +function isOutOfView(element: HTMLElement, container: HTMLElement) { + const tocItemTop = element.offsetTop; + const containerTop = container.scrollTop; + const containerBottom = containerTop + container.clientHeight; return ( tocItemTop < containerTop + TOC_ITEM_OFFSET || tocItemTop > containerBottom - TOC_ITEM_OFFSET diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 9f9c0d85da..05ee56160c 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -66,12 +66,7 @@ export function TableOfContents(props: { <div // The actual sidebar, either shown with a filled bg or transparent. className={tcls( 'lg:-ms-5', - 'overflow-hidden', - 'relative', - - 'flex', - 'flex-col', - 'flex-grow', + 'relative flex flex-grow flex-col overflow-hidden', 'sidebar-filled:bg-tint-subtle', 'theme-muted:bg-tint-subtle', @@ -84,18 +79,12 @@ export function TableOfContents(props: { 'straight-corners:rounded-none' )} > - {innerHeader && <div className={tcls('px-5 *:my-4')}>{innerHeader}</div>} + {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>} <TOCScrollContainer // The scrollview inside the sidebar className={tcls( - 'flex', - 'flex-grow', - 'flex-col', - - 'p-2', + 'flex flex-grow flex-col p-2', customization.trademark.enabled && 'lg:pb-20', - - 'overflow-y-auto', - 'lg:gutter-stable', + 'lg:gutter-stable overflow-y-auto', '[&::-webkit-scrollbar]:bg-transparent', '[&::-webkit-scrollbar-thumb]:bg-transparent', 'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle', @@ -107,11 +96,7 @@ export function TableOfContents(props: { rootPages={pages} pages={pages} context={context} - style={tcls( - 'page-no-toc:hidden', - 'sidebar-list-line:border-l', - 'border-tint-subtle' - )} + style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l" /> {customization.trademark.enabled ? ( <Trademark diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index aa4700482b..38d6674514 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -2,12 +2,12 @@ import { Icon } from '@gitbook/icons'; import { motion } from 'framer-motion'; -import React from 'react'; +import React, { useRef } from 'react'; import { tcls } from '@/lib/tailwind'; -import { useCurrentPagePath, useToggleAnimation } from '../hooks'; -import { Link, type LinkInsightsProps } from '../primitives'; +import { useCurrentPagePath } from '../hooks'; +import { Link, type LinkInsightsProps, type LinkProps } from '../primitives'; import { useScrollToActiveTOCItem } from './TOCScroller'; /** @@ -24,128 +24,190 @@ export function ToggleableLinkItem( const { href, children, descendants, pathname, insights } = props; const currentPagePath = useCurrentPagePath(); - const isActive = currentPagePath === pathname; - const hasDescendants = !!descendants; - const hasActiveDescendant = - hasDescendants && (isActive || currentPagePath.startsWith(`${pathname}/`)); - const [isVisible, setIsVisible] = React.useState(hasActiveDescendant); + if (!descendants) { + return ( + <LinkItem href={href} insights={insights} isActive={isActive}> + {children} + </LinkItem> + ); + } - // Update the visibility of the children, if we are navigating to a descendant. - React.useEffect(() => { - if (!hasDescendants) { - return; - } + return ( + <DescendantsRenderer + descendants={descendants} + defaultIsOpen={isActive || currentPagePath.startsWith(`${pathname}/`)} + > + {({ descendants, toggler }) => ( + <> + <LinkItem href={href} insights={insights} isActive={isActive}> + {children} + {toggler} + </LinkItem> + {descendants} + </> + )} + </DescendantsRenderer> + ); +} - setIsVisible((prev) => prev || hasActiveDescendant); - }, [hasActiveDescendant, hasDescendants]); +function LinkItem( + props: Pick<LinkProps, 'href' | 'insights' | 'children'> & { + isActive: boolean; + } +) { + const { isActive, href, insights, children } = props; + const anchorRef = useRef<HTMLAnchorElement>(null); + useScrollToActiveTOCItem({ anchorRef, isActive }); + return ( + <Link + ref={anchorRef} + href={href} + insights={insights} + aria-current={isActive ? 'page' : undefined} + className={tcls( + 'group/toclink relative transition-colors', + 'flex flex-row justify-between', + 'rounded-md straight-corners:rounded-none p-1.5 pl-3', + 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', + 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12', + 'before:contents[] before:-left-px before:absolute before:inset-y-0', + 'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px', + + isActive && [ + 'font-semibold', + 'sidebar-list-line:before:w-0.5', + + 'before:bg-primary-solid', + 'text-primary-subtle', + 'contrast-more:text-primary', + + 'sidebar-list-pill:bg-primary', + '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover', + '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover', + '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary', + '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary', + + 'hover:bg-primary-hover', + 'hover:text-primary', + 'hover:before:bg-primary-solid-hover', + 'sidebar-list-pill:hover:bg-primary-hover', + + 'contrast-more:text-primary', + 'contrast-more:hover:text-primary-strong', + 'contrast-more:bg-primary', + 'contrast-more:ring-1', + 'contrast-more:ring-primary', + 'contrast-more:hover:ring-primary-hover', + ] + )} + > + {children} + </Link> + ); +} - const { show, hide, scope } = useToggleAnimation({ hasDescendants, isVisible }); +function DescendantsRenderer(props: { + defaultIsOpen: boolean; + descendants: React.ReactNode; + children: (renderProps: { + descendants: React.ReactNode; + toggler: React.ReactNode; + }) => React.ReactNode; +}) { + const { defaultIsOpen, children, descendants } = props; + const [isOpen, setIsOpen] = React.useState(defaultIsOpen); - const linkRef = React.createRef<HTMLAnchorElement>(); - useScrollToActiveTOCItem({ linkRef, isActive }); + // Update the visibility of the children if one of the descendants becomes active. + React.useEffect(() => { + if (defaultIsOpen) { + setIsOpen(defaultIsOpen); + } + }, [defaultIsOpen]); + + return children({ + toggler: ( + <Toggler + isLinkActive={isOpen} + isOpen={isOpen} + onToggle={() => { + setIsOpen((prev) => !prev); + }} + /> + ), + descendants: <Descendants isVisible={isOpen}>{descendants}</Descendants>, + }); +} +function Toggler(props: { + isLinkActive: boolean; + isOpen: boolean; + onToggle: () => void; +}) { + const { isLinkActive, isOpen, onToggle } = props; return ( - <> - <Link - ref={linkRef} - href={href} - insights={insights} - {...(isActive ? { 'aria-current': 'page' } : {})} + <span + className={tcls( + 'group', + 'relative', + 'rounded-full', + 'straight-corners:rounded-sm', + 'w-5', + 'h-5', + 'after:grid-area-1-1', + 'after:absolute', + 'after:-top-1', + 'after:grid', + 'after:-left-1', + 'after:w-7', + 'after:h-7', + 'hover:bg-tint-active', + 'hover:text-current', + isLinkActive && 'hover:bg-tint-hover' + )} + onClick={(event) => { + event.preventDefault(); + event.stopPropagation(); + onToggle(); + }} + > + <Icon + icon="chevron-right" className={tcls( - 'group/toclink relative transition-colors', - 'flex flex-row justify-between', - 'rounded-md straight-corners:rounded-none p-1.5 pl-3', - 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', - 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12', - 'before:contents[] before:-left-px before:absolute before:inset-y-0', - 'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px', - - isActive && [ - 'font-semibold', - 'sidebar-list-line:before:w-0.5', - - 'before:bg-primary-solid', - 'text-primary-subtle', - 'contrast-more:text-primary', - - 'sidebar-list-pill:bg-primary', - '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover', - '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover', - '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary', - '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary', - - 'hover:bg-primary-hover', - 'hover:text-primary', - 'hover:before:bg-primary-solid-hover', - 'sidebar-list-pill:hover:bg-primary-hover', - - 'contrast-more:text-primary', - 'contrast-more:hover:text-primary-strong', - 'contrast-more:bg-primary', - 'contrast-more:ring-1', - 'contrast-more:ring-primary', - 'contrast-more:hover:ring-primary-hover', - ] + 'm-1 grid size-3 flex-shrink-0 text-current opacity-6 transition', + 'group-hover:opacity-11 contrast-more:opacity-11', + isOpen ? 'rotate-90' : 'rotate-0' )} - > - {children} - {hasDescendants ? ( - <span - className={tcls( - 'group', - 'relative', - 'rounded-full', - 'straight-corners:rounded-sm', - 'w-5', - 'h-5', - 'after:grid-area-1-1', - 'after:absolute', - 'after:-top-1', - 'after:grid', - 'after:-left-1', - 'after:w-7', - 'after:h-7', - 'hover:bg-tint-active', - 'hover:text-current', - isActive ? ['hover:bg-tint-hover'] : [] - )} - onClick={(event) => { - event.preventDefault(); - event.stopPropagation(); - setIsVisible((prev) => !prev); - }} - > - <Icon - icon="chevron-right" - className={tcls( - 'grid', - 'flex-shrink-0', - 'size-3', - 'm-1', - 'transition-[opacity]', - 'text-current', - 'transition-transform', - 'opacity-6', - 'group-hover:opacity-11', - 'contrast-more:opacity-11', - - isVisible ? ['rotate-90'] : ['rotate-0'] - )} - /> - </span> - ) : null} - </Link> - {hasDescendants ? ( - <motion.div - ref={scope} - className={tcls(isVisible ? null : '[&_ul>li]:opacity-1')} - initial={isVisible ? show : hide} - > - {descendants} - </motion.div> - ) : null} - </> + /> + </span> + ); +} + +const show = { + opacity: 1, + height: 'auto', +}; +const hide = { + opacity: 0, + height: 0, + transitionEnd: { + display: 'none', + }, +}; + +function Descendants(props: { + isVisible: boolean; + children: React.ReactNode; +}) { + const { isVisible, children } = props; + return ( + <motion.div + className="overflow-hidden" + animate={isVisible ? show : hide} + initial={isVisible ? show : hide} + > + {children} + </motion.div> ); } diff --git a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts index f1f719ddd7..23943e00b0 100644 --- a/packages/gitbook/src/components/hooks/useCurrentPagePath.ts +++ b/packages/gitbook/src/components/hooks/useCurrentPagePath.ts @@ -3,6 +3,7 @@ import { useParams, useSelectedLayoutSegment } from 'next/navigation'; import { removeLeadingSlash } from '@/lib/paths'; +import { useMemo } from 'react'; /** * Return the page of the current page being rendered. @@ -14,9 +15,11 @@ export function useCurrentPagePath() { // For V1, we use the selected layout segment. const rawActiveSegment = useSelectedLayoutSegment() ?? ''; - if (typeof params.pagePath === 'string') { - return removeLeadingSlash(decodeURIComponent(params.pagePath)); - } + return useMemo(() => { + if (typeof params.pagePath === 'string') { + return removeLeadingSlash(decodeURIComponent(params.pagePath)); + } - return decodeURIComponent(rawActiveSegment); + return decodeURIComponent(rawActiveSegment); + }, [params.pagePath, rawActiveSegment]); } diff --git a/packages/gitbook/src/components/hooks/useToggleAnimation.ts b/packages/gitbook/src/components/hooks/useToggleAnimation.ts index c028155142..3c9c02fad7 100644 --- a/packages/gitbook/src/components/hooks/useToggleAnimation.ts +++ b/packages/gitbook/src/components/hooks/useToggleAnimation.ts @@ -1,7 +1,7 @@ -import { stagger, useAnimate } from 'framer-motion'; -import React from 'react'; +'use client'; -import { useIsMounted } from '.'; +import { stagger, useAnimate } from 'framer-motion'; +import React, { useLayoutEffect } from 'react'; const show = { opacity: 1, @@ -19,44 +19,35 @@ const hide = { const staggerMenuItems = stagger(0.02, { ease: (p) => p ** 2 }); -export function useToggleAnimation({ - hasDescendants, - isVisible, -}: { - hasDescendants: boolean; - isVisible: boolean; -}) { - const isMounted = useIsMounted(); +export function useToggleAnimation(isVisible: boolean) { const [scope, animate] = useAnimate(); + const previousIsVisibleRef = React.useRef<boolean | undefined>(undefined); + useLayoutEffect(() => { + previousIsVisibleRef.current = isVisible; + }); + const previousIsVisible = previousIsVisibleRef.current; // Animate the visibility of the children // only after the initial state. React.useEffect(() => { - if (!isMounted || !hasDescendants) { + if (previousIsVisible === undefined || previousIsVisible === isVisible) { return; } + try { - animate(scope.current, isVisible ? show : hide, { - duration: 0.1, - }); + animate(scope.current, isVisible ? show : hide, { duration: 0.1 }); const selector = '& > ul > li'; - if (isVisible) - animate( - selector, - { opacity: 1 }, - { - delay: staggerMenuItems, - } - ); - else { + if (isVisible) { + animate(selector, { opacity: 1 }, { delay: staggerMenuItems }); + } else { animate(selector, { opacity: 0 }); } } catch (error) { // The selector can crash in some browsers, we ignore it as the animation is not critical. console.error(error); } - }, [isVisible, isMounted, hasDescendants, animate, scope]); + }, [previousIsVisible, isVisible, animate, scope]); return { show, hide, scope }; } From 634e0b43029e85e97b1ef42da66e5a781a08d091 Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Wed, 23 Apr 2025 12:47:28 +0100 Subject: [PATCH 002/127] Improve error messages around undefined site sections. (#3179) --- .changeset/soft-planes-talk.md | 5 +++ packages/gitbook-v2/src/lib/context.ts | 51 ++++++++++++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 .changeset/soft-planes-talk.md diff --git a/.changeset/soft-planes-talk.md b/.changeset/soft-planes-talk.md new file mode 100644 index 0000000000..d633d90dcd --- /dev/null +++ b/.changeset/soft-planes-talk.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Improve error messages around undefined site sections. diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index d25dd1946e..7ef0f463b9 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -19,6 +19,7 @@ import { getDataOrNull, throwIfDataError, } from '@v2/lib/data'; +import assertNever from 'assert-never'; import { notFound } from 'next/navigation'; import { assert } from 'ts-essentials'; import { GITBOOK_URL } from './env'; @@ -242,19 +243,45 @@ export async function fetchSiteContextByIds( ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection) : null; - const siteSpace = ( - siteStructure.type === 'siteSpaces' && siteStructure.structure - ? siteStructure.structure - : sections?.current.siteSpaces - )?.find((siteSpace) => siteSpace.id === ids.siteSpace); - if (!siteSpace) { - throw new Error('Site space not found'); - } + // Parse the current siteSpace and siteSpaces based on the site structure type. + const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => { + if (siteStructure.type === 'siteSpaces') { + const siteSpaces = siteStructure.structure; + const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace); + + if (!siteSpace) { + throw new Error( + `Site space "${ids.siteSpace}" not found in structure type="siteSpaces"` + ); + } + + return { siteSpaces, siteSpace }; + } + + if (siteStructure.type === 'sections') { + assert( + sections, + `cannot find site space "${ids.siteSpace}" because parsed sections are missing siteStructure.type="sections" siteSection="${ids.siteSection}"` + ); - const siteSpaces = - siteStructure.type === 'siteSpaces' - ? siteStructure.structure - : (sections?.current.siteSpaces ?? []); + const currentSection = sections.current; + const siteSpaces = currentSection.siteSpaces; + const siteSpace = currentSection.siteSpaces.find( + (siteSpace) => siteSpace.id === ids.siteSpace + ); + + if (!siteSpace) { + throw new Error( + `Site space "${ids.siteSpace}" not found in structure type="sections" currentSection="${currentSection.id}"` + ); + } + + return { siteSpaces, siteSpace }; + } + + // @ts-expect-error + assertNever(siteStructure, `cannot handle site structure of type ${siteStructure.type}`); + })(); const customization = (() => { if (ids.siteSpace) { From 97b7c79bc6a69ca6739b6e593e6f3b5b2f0c3c82 Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Wed, 23 Apr 2025 15:04:00 +0100 Subject: [PATCH 003/127] Increase logging around caching behaviour causing page crashes. (#3182) --- .changeset/sweet-schools-flow.md | 6 ++++++ packages/gitbook-v2/src/lib/context.ts | 20 +++++++++++++++++++- packages/gitbook/src/middleware.ts | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .changeset/sweet-schools-flow.md diff --git a/.changeset/sweet-schools-flow.md b/.changeset/sweet-schools-flow.md new file mode 100644 index 0000000000..6ff44bfa96 --- /dev/null +++ b/.changeset/sweet-schools-flow.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": patch +"gitbook": patch +--- + +Increase logging around caching behaviour causing page crashes. diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index 7ef0f463b9..c85434c91f 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -220,6 +220,8 @@ export async function fetchSiteContextByIds( ): Promise<GitBookSiteContext> { const { dataFetcher } = baseContext; + const DEBUG = ids.site === 'site_cu2ih'; + const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] = await Promise.all([ throwIfDataError( @@ -232,6 +234,22 @@ export async function fetchSiteContextByIds( fetchSpaceContextByIds(baseContext, ids), ]); + DEBUG && console.log('ids', JSON.stringify(ids)); + DEBUG && + console.log( + 'siteStructure', + siteStructure + ? JSON.stringify({ + type: siteStructure.type, + structure: siteStructure.structure.map((s) => { + // @ts-ignore + const { siteSpaces, urls, ...rest } = s; + return rest; + }), + }) + : 'null' + ); + // override the title with the customization title // TODO: remove this hack once we have a proper way to handle site customizations const site = { @@ -407,7 +425,7 @@ export function checkIsRootSiteContext(context: GitBookSiteContext): boolean { function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false }); const section = parseCurrentSection(structure, siteSectionId); - assert(section, 'A section must be defined when there are multiple sections'); + assert(section, `couldn't find section "${siteSectionId}" in site structure`); return { list: sectionsAndGroups, current: section } satisfies SiteSections; } diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 30f7b44e6a..0e599c4486 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -674,6 +674,9 @@ async function lookupSiteByAPI( } ); + alternative.url.includes('minas-tirith') && + console.log(`resolved ${alternative.url} -> ${JSON.stringify(data)}`); + if ('error' in data) { if (alternative.primary) { // We only return an error for the primary alternative (full URL), From dd043df00f2619a2448d08b16a4eaa3986574898 Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Wed, 23 Apr 2025 16:53:33 +0100 Subject: [PATCH 004/127] Revert investigation work around URL caches. (#3183) --- .changeset/young-games-talk.md | 6 ++++++ packages/gitbook-v2/src/lib/context.ts | 18 ------------------ packages/gitbook/src/middleware.ts | 3 --- 3 files changed, 6 insertions(+), 21 deletions(-) create mode 100644 .changeset/young-games-talk.md diff --git a/.changeset/young-games-talk.md b/.changeset/young-games-talk.md new file mode 100644 index 0000000000..096dc02060 --- /dev/null +++ b/.changeset/young-games-talk.md @@ -0,0 +1,6 @@ +--- +"gitbook": patch +"gitbook-v2": patch +--- + +Revert investigation work around URL caches. diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index c85434c91f..22e25a5990 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -220,8 +220,6 @@ export async function fetchSiteContextByIds( ): Promise<GitBookSiteContext> { const { dataFetcher } = baseContext; - const DEBUG = ids.site === 'site_cu2ih'; - const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] = await Promise.all([ throwIfDataError( @@ -234,22 +232,6 @@ export async function fetchSiteContextByIds( fetchSpaceContextByIds(baseContext, ids), ]); - DEBUG && console.log('ids', JSON.stringify(ids)); - DEBUG && - console.log( - 'siteStructure', - siteStructure - ? JSON.stringify({ - type: siteStructure.type, - structure: siteStructure.structure.map((s) => { - // @ts-ignore - const { siteSpaces, urls, ...rest } = s; - return rest; - }), - }) - : 'null' - ); - // override the title with the customization title // TODO: remove this hack once we have a proper way to handle site customizations const site = { diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 0e599c4486..30f7b44e6a 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -674,9 +674,6 @@ async function lookupSiteByAPI( } ); - alternative.url.includes('minas-tirith') && - console.log(`resolved ${alternative.url} -> ${JSON.stringify(data)}`); - if ('error' in data) { if (alternative.primary) { // We only return an error for the primary alternative (full URL), From 0353a8186cd8388fd0cc8dcebb748f97b9cde2b8 Mon Sep 17 00:00:00 2001 From: Viktor Renkema <49148610+viktorrenkema@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:43:20 +0200 Subject: [PATCH 005/127] Have all nodes within a text-cells adhere to alignments (#3175) --- .../DocumentView/Table/RecordColumnValue.tsx | 5 +- .../DocumentView/Table/ViewGrid.tsx | 48 +++++++++---------- .../DocumentView/Table/table.module.css | 2 +- .../components/DocumentView/Table/utils.ts | 20 ++++++-- .../src/components/primitives/Button.tsx | 2 +- packages/gitbook/tailwind.config.ts | 1 + 6 files changed, 44 insertions(+), 34 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index d5ddcdc491..aa1bf45b86 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -115,7 +115,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>( return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>; } - const alignment = getColumnAlignment(definition); + const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`; return ( <Blocks @@ -130,8 +130,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>( 'lg:space-y-3', 'leading-normal', verticalAlignment, - alignment === 'right' ? 'text-right' : null, - alignment === 'center' ? 'text-center' : null, + horizontalAlignment, ]} context={context} blockStyle={['w-full', 'max-w-[unset]']} diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx index 65bae8e0b8..50aa13565b 100644 --- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx @@ -42,32 +42,28 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) { )} > <div role="row" className={tcls('flex', 'w-full')}> - {view.columns.map((column) => { - const alignment = getColumnAlignment(block.data.definition[column]); - return ( - <div - key={column} - role="columnheader" - className={tcls( - styles.columnHeader, - alignment === 'right' ? 'text-right' : null, - alignment === 'center' ? 'text-center' : null - )} - style={{ - width: getColumnWidth({ - column, - columnWidths, - autoSizedColumns, - fixedColumns, - }), - minWidth: columnWidths?.[column] || '100px', - }} - title={block.data.definition[column].title} - > - {block.data.definition[column].title} - </div> - ); - })} + {view.columns.map((column) => ( + <div + key={column} + role="columnheader" + className={tcls( + styles.columnHeader, + getColumnAlignment(block.data.definition[column]) + )} + style={{ + width: getColumnWidth({ + column, + columnWidths, + autoSizedColumns, + fixedColumns, + }), + minWidth: columnWidths?.[column] || '100px', + }} + title={block.data.definition[column].title} + > + {block.data.definition[column].title} + </div> + ))} </div> </div> )} diff --git a/packages/gitbook/src/components/DocumentView/Table/table.module.css b/packages/gitbook/src/components/DocumentView/Table/table.module.css index 4b602a607c..53065b9d67 100644 --- a/packages/gitbook/src/components/DocumentView/Table/table.module.css +++ b/packages/gitbook/src/components/DocumentView/Table/table.module.css @@ -23,7 +23,7 @@ } .columnHeader { - @apply text-sm font-medium py-2 px-4 text-tint-strong; + @apply text-sm font-medium py-2 px-3 text-tint-strong; } .row { diff --git a/packages/gitbook/src/components/DocumentView/Table/utils.ts b/packages/gitbook/src/components/DocumentView/Table/utils.ts index 01bd82bcd6..1fc95b357e 100644 --- a/packages/gitbook/src/components/DocumentView/Table/utils.ts +++ b/packages/gitbook/src/components/DocumentView/Table/utils.ts @@ -1,4 +1,5 @@ import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api'; +import assertNever from 'assert-never'; /** * Get the value for a column in a record. @@ -14,11 +15,24 @@ export function getRecordValue<T extends number | string | boolean | string[] | /** * Get the text alignment for a column. */ -export function getColumnAlignment(column: DocumentTableDefinition): 'left' | 'right' | 'center' { +export function getColumnAlignment(column: DocumentTableDefinition) { + const defaultAlignment = 'text-left'; + if (column.type === 'text') { - return column.textAlignment ?? 'left'; + switch (column.textAlignment) { + case undefined: + case 'left': + return defaultAlignment; + case 'center': + return 'text-center'; + case 'right': + return 'text-right'; + default: + assertNever(column.textAlignment); + } } - return 'left'; + + return defaultAlignment; } /** diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx index 01d8880077..b7edac7ab0 100644 --- a/packages/gitbook/src/components/primitives/Button.tsx +++ b/packages/gitbook/src/components/primitives/Button.tsx @@ -90,7 +90,7 @@ export function Button({ 'contrast-more:hover:ring-2', 'contrast-more:hover:ring-tint-12', - 'hover:scale-105', + 'hover:scale-104', 'active:scale-100', 'transition-all', diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 7a05bd15fc..2e5b7c359f 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -443,6 +443,7 @@ const config: Config = { scale: { '98': '0.98', '102': '1.02', + '104': '1.04', }, }, opacity: opacity(), From 90f0127ada2491b88ef48b754e17c2ba43003c4a Mon Sep 17 00:00:00 2001 From: Johan Preynat <johan.preynat@gmail.com> Date: Thu, 24 Apr 2025 16:53:23 +0200 Subject: [PATCH 006/127] Use react@19 on v2 and v1 (#3184) --- bun.lock | 190 +++++++--------------------------- package.json | 6 +- packages/gitbook/package.json | 6 +- 3 files changed, 43 insertions(+), 159 deletions(-) diff --git a/bun.lock b/bun.lock index 323725884f..5476639847 100644 --- a/bun.lock +++ b/bun.lock @@ -49,7 +49,7 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.10.1", + "version": "0.11.1", "dependencies": { "@gitbook/api": "*", "@gitbook/cache-do": "workspace:*", @@ -89,8 +89,8 @@ "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", "partial-json": "^0.1.7", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.4.1", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -111,7 +111,7 @@ }, "devDependencies": { "@argos-ci/playwright": "^5.0.3", - "@cloudflare/next-on-pages": "1.13.7", + "@cloudflare/next-on-pages": "1.13.12", "@cloudflare/workers-types": "^4.20241230.0", "@playwright/test": "^1.51.1", "@types/js-cookie": "^3.0.6", @@ -231,7 +231,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.1.10", + "version": "1.2.1", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "^1.2.19", @@ -260,8 +260,8 @@ "overrides": { "@codemirror/state": "6.4.1", "@gitbook/api": "0.111.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -472,7 +472,7 @@ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], - "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.7", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-TSMVy+1fmxzeyykOC9guMEj7G2FgENw1T8V1sIFnah6piaJNBmybdyikPdQSdikP5w6v9eQhBt/TrXDPMDw0dw=="], + "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.12", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-rPy7x9c2+0RDDdJ5o0TeRUwXJ1b7N1epnqF6qKSp5Wz1r9KHOyvaZh1ACoOC6Vu5k9su5WZOgy+8fPLIyrldMQ=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250320.0" }, "optionalPeers": ["workerd"] }, "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg=="], @@ -546,10 +546,6 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], - - "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], @@ -1094,7 +1090,7 @@ "@scalar/object-utils": ["@scalar/object-utils@1.1.13", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1" } }, "sha512-311eTykIXgOtjCs4VTELj9UMT97jHTWc5qkGNoIzZ5nxjCcvOVe7kDQobIkE8dGT+ybOgHz5qly02Eu7nVHeZQ=="], - "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.10", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-6MSgvpNKu/anZy96dn8tXQZo1PuDCoeB4m2ZLLDS4vC2zaTnuNBvvQHx+gjwXNKWhTbIVy8bQpYBzlMAYnFNcQ=="], + "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="], "@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], @@ -1390,7 +1386,7 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -1510,8 +1506,6 @@ "caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="], - "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -1564,8 +1558,6 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "configstore": ["configstore@5.0.1", "", { "dependencies": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" } }, "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA=="], "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], @@ -1756,7 +1748,7 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], @@ -2308,8 +2300,6 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], - "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -2454,8 +2444,6 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "playwright": ["playwright@1.51.1", "", { "dependencies": { "playwright-core": "1.51.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw=="], "playwright-core": ["playwright-core@1.51.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw=="], @@ -2518,13 +2506,13 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-aria": ["react-aria@3.37.0", "", { "dependencies": { "@internationalized/string": "^3.2.5", "@react-aria/breadcrumbs": "^3.5.20", "@react-aria/button": "^3.11.1", "@react-aria/calendar": "^3.7.0", "@react-aria/checkbox": "^3.15.1", "@react-aria/color": "^3.0.3", "@react-aria/combobox": "^3.11.1", "@react-aria/datepicker": "^3.13.0", "@react-aria/dialog": "^3.5.21", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/gridlist": "^3.10.1", "@react-aria/i18n": "^3.12.5", "@react-aria/interactions": "^3.23.0", "@react-aria/label": "^3.7.14", "@react-aria/link": "^3.7.8", "@react-aria/listbox": "^3.14.0", "@react-aria/menu": "^3.17.0", "@react-aria/meter": "^3.4.19", "@react-aria/numberfield": "^3.11.10", "@react-aria/overlays": "^3.25.0", "@react-aria/progress": "^3.4.19", "@react-aria/radio": "^3.10.11", "@react-aria/searchfield": "^3.8.0", "@react-aria/select": "^3.15.1", "@react-aria/selection": "^3.22.0", "@react-aria/separator": "^3.4.5", "@react-aria/slider": "^3.7.15", "@react-aria/ssr": "^3.9.7", "@react-aria/switch": "^3.6.11", "@react-aria/table": "^3.16.1", "@react-aria/tabs": "^3.9.9", "@react-aria/tag": "^3.4.9", "@react-aria/textfield": "^3.16.0", "@react-aria/tooltip": "^3.7.11", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-u3WUEMTcbQFaoHauHO3KhPaBYzEv1o42EdPcLAs05GBw9Q6Axlqwo73UFgMrsc2ElwLAZ4EKpSdWHLo1R5gfiw=="], "react-aria-components": ["react-aria-components@1.6.0", "", { "dependencies": { "@internationalized/date": "^3.7.0", "@internationalized/string": "^3.2.5", "@react-aria/autocomplete": "3.0.0-alpha.37", "@react-aria/collections": "3.0.0-alpha.7", "@react-aria/color": "^3.0.3", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", "@react-aria/menu": "^3.17.0", "@react-aria/toolbar": "3.0.0-beta.12", "@react-aria/tree": "3.0.0-beta.3", "@react-aria/utils": "^3.27.0", "@react-aria/virtualizer": "^4.1.1", "@react-stately/autocomplete": "3.0.0-alpha.0", "@react-stately/color": "^3.8.2", "@react-stately/disclosure": "^3.0.1", "@react-stately/layout": "^4.1.1", "@react-stately/menu": "^3.9.1", "@react-stately/selection": "^3.19.0", "@react-stately/table": "^3.13.1", "@react-stately/utils": "^3.10.5", "@react-stately/virtualizer": "^4.2.1", "@react-types/color": "^3.0.2", "@react-types/form": "^3.7.9", "@react-types/grid": "^3.2.11", "@react-types/shared": "^3.27.0", "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.37.0", "react-stately": "^3.35.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YfG9PUE7XrXtDDAqT4pLTGyYQaiHHTBFdAK/wNgGsypVnQSdzmyYlV3Ty8aHlZJI6hP9RWkbywvosXkU7KcPHg=="], - "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="], @@ -2598,12 +2586,6 @@ "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], - "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], - - "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], - - "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], - "router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -2612,7 +2594,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], @@ -2660,8 +2642,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], @@ -3558,15 +3538,13 @@ "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "@cloudflare/next-on-pages/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], - "@cloudflare/next-on-pages/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "@cloudflare/next-on-pages/esbuild": ["esbuild@0.15.18", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="], - "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="], + "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="], - "@cloudflare/next-on-pages/wrangler": ["wrangler@3.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250214.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.1", "workerd": "1.20250214.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250214.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA=="], + "@cloudflare/next-on-pages/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], @@ -3714,14 +3692,10 @@ "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "@scalar/api-client/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="], - "@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], "@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], - "@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="], - "@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], "@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], @@ -3922,8 +3896,6 @@ "@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - "@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "@vercel/nft/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@vercel/nft/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -3980,8 +3952,6 @@ "cacheable-request/lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "capnp-ts/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "codemirror/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], "codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="], @@ -4032,7 +4002,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook-v2/next": ["next@15.3.1-canary.8", "", { "dependencies": { "@next/env": "15.3.1-canary.8", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.8", "@next/swc-darwin-x64": "15.3.1-canary.8", "@next/swc-linux-arm64-gnu": "15.3.1-canary.8", "@next/swc-linux-arm64-musl": "15.3.1-canary.8", "@next/swc-linux-x64-gnu": "15.3.1-canary.8", "@next/swc-linux-x64-musl": "15.3.1-canary.8", "@next/swc-win32-arm64-msvc": "15.3.1-canary.8", "@next/swc-win32-x64-msvc": "15.3.1-canary.8", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-Of5a3BTTIl/iUvL2a9Jh7m7G/H8z4Pj5Vs54CLvcdadokxSNgLOpjzbDgFR8J4PawLx6+MOMy19m9Cvr6EPGug=="], + "gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="], "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="], @@ -4070,8 +4040,6 @@ "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], @@ -4090,8 +4058,6 @@ "micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], @@ -4102,8 +4068,6 @@ "minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="], - "mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -4156,12 +4120,6 @@ "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - - "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], - - "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], @@ -4200,10 +4158,10 @@ "terminal-link/supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], - "terser/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "ts-node/acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], + "ts-node/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], @@ -4650,21 +4608,13 @@ "@cloudflare/next-on-pages/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="], - "@cloudflare/next-on-pages/miniflare/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], - - "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="], - - "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], - - "@cloudflare/next-on-pages/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + "@cloudflare/next-on-pages/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], - "@cloudflare/next-on-pages/wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="], - "@cloudflare/next-on-pages/wrangler/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="], + "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], - "@cloudflare/next-on-pages/wrangler/unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="], - - "@cloudflare/next-on-pages/wrangler/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="], + "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], "@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="], @@ -4908,23 +4858,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.8", "", {}, "sha512-ShZTo0hNhbTRrp7k6oUDSck4Xx4hhfSeLBp35jvGaw1QMZzWYr5v/oc0kEt0bfMdl+833flwKV7kFR3BnrULfg=="], + "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZaDynM+pbrnLLlBAxH/CDGp9KN79OFrLcT1ejlWyo86V3SS9Gyqr4nmTuvTevByTTpr1VHReQel8Zbq0Pttu7Q=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-BsMR8WqeCDAX8C9RYAO8TI4ttpuqKk2oYMb1+bCrOYi857SMfB4vWD4PWmMvj7mwFXGqrxly4W6CBQlD66A+fg=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-xL+K2SW+/46j/KnKNf1gizM1bxwcEaE56eCEG9RPoYS/lfxHLuHcR9O2MlcPI90g/rIN2HXmHeuKRbXbnVy15g=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-bzXlCUXkjIRsMTb6rr7OsWEQmdO2rZgKijnMGBJzEpa9ROq95VJTF+rdyrLmHuc4fPsAGDn/C18V3E1YOi4ipQ=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-snbPQ9th7eoYYMZpNGhEvX4EbqGjjkiXzTEm2F/oDadlZR2wI6egtfQHiZqeczL7XwG3M66hBJ1/U20wdTDtvA=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.8", "", { "os": "linux", "cpu": "x64" }, "sha512-5hmcaGazc3w6rg/gbQOrmuw0kKShf4Egs0JlUPop/ZiRDfZO5KlyrGY0hUD+2pLG7Yx3B/fTbj86cHKfQZDMBw=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-t6uwWC/UbQ8CQGyBVbEmpcJC41yAvz2eZZGec9EoO0ZyQ/fbvjOkqfPnf+WcjWifWNeuiWogjN1UBg7YK9IaVA=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.8", "", { "os": "win32", "cpu": "x64" }, "sha512-93gNqVYwlr9bz6Pm5dQnqjpuOEQyvDre0+H39UqS7h2KuFh3sf2MejhAEr2uFEux4mb7TN0PlohaihO7YxJ3fw=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -5114,81 +5064,15 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="], - - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="], - - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="], - - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="], - - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], - - "@cloudflare/next-on-pages/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], - - "@cloudflare/next-on-pages/wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - - "@cloudflare/next-on-pages/wrangler/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], - - "@cloudflare/next-on-pages/wrangler/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], - - "@cloudflare/next-on-pages/wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], - - "@cloudflare/next-on-pages/wrangler/unenv/ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], - - "@cloudflare/next-on-pages/wrangler/unenv/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - - "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="], - "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="], - "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="], - "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="], - "@cloudflare/next-on-pages/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="], "@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], diff --git a/package.json b/package.json index 736eabe411..fba8d79576 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "packageManager": "bun@1.2.8", "overrides": { "@codemirror/state": "6.4.1", - "react": "18.3.1", - "react-dom": "18.3.1", - "@gitbook/api": "0.111.0" + "@gitbook/api": "0.111.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "private": true, "scripts": { diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index d0abc7512b..6f155974f7 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -52,8 +52,8 @@ "openapi-types": "^12.1.3", "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.4.1", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -76,7 +76,7 @@ }, "devDependencies": { "@argos-ci/playwright": "^5.0.3", - "@cloudflare/next-on-pages": "1.13.7", + "@cloudflare/next-on-pages": "1.13.12", "@cloudflare/workers-types": "^4.20241230.0", "@playwright/test": "^1.51.1", "@types/js-cookie": "^3.0.6", From 326e28e9b06a3d72879f1196d68bf4563faf63df Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Thu, 24 Apr 2025 16:53:29 +0200 Subject: [PATCH 007/127] Restyle openapi blocks (#3169) --- .changeset/lemon-actors-sing.md | 6 + .../CodeBlock/CodeBlockRenderer.css | 2 +- .../CodeBlock/CodeBlockRenderer.tsx | 6 +- .../DocumentView/CodeBlock/theme.css | 86 +++++-- .../components/DocumentView/OpenAPI/style.css | 219 ++++++++++-------- .../react-openapi/src/OpenAPIDisclosure.tsx | 10 +- .../src/OpenAPIDisclosureGroup.tsx | 97 ++++---- .../react-openapi/src/OpenAPIResponse.tsx | 36 ++- packages/react-openapi/src/OpenAPISchema.tsx | 185 ++++++++------- .../react-openapi/src/OpenAPISchemaName.tsx | 4 +- packages/react-openapi/src/StaticSection.tsx | 2 +- .../src/schemas/OpenAPISchemas.tsx | 7 +- packages/react-openapi/src/translations/de.ts | 13 +- packages/react-openapi/src/translations/en.ts | 11 +- packages/react-openapi/src/translations/es.ts | 13 +- packages/react-openapi/src/translations/fr.ts | 15 +- packages/react-openapi/src/translations/ja.ts | 11 +- packages/react-openapi/src/translations/nl.ts | 11 +- packages/react-openapi/src/translations/no.ts | 13 +- .../react-openapi/src/translations/pt-br.ts | 13 +- packages/react-openapi/src/translations/zh.ts | 13 +- 21 files changed, 468 insertions(+), 305 deletions(-) create mode 100644 .changeset/lemon-actors-sing.md diff --git a/.changeset/lemon-actors-sing.md b/.changeset/lemon-actors-sing.md new file mode 100644 index 0000000000..d212947861 --- /dev/null +++ b/.changeset/lemon-actors-sing.md @@ -0,0 +1,6 @@ +--- +"@gitbook/react-openapi": minor +"gitbook": minor +--- + +Design tweaks to code blocks and OpenAPI pages diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css index 6a56801796..71d75354eb 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css @@ -24,7 +24,7 @@ } .highlight-line-number { - @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint to-transparent; + @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent; @apply before:text-tint before:content-[counter(line)]; .highlight-line.highlighted > & { diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx index f1f8cb8f63..d8f35d90bb 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx @@ -39,7 +39,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( > <div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]"> {title ? ( - <div className="inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s bg-tint px-3 py-2 text-tint text-xs leading-none tracking-wide"> + <div className="relative top-px z-20 inline-flex items-center justify-center rounded-t straight-corners:rounded-t-s border border-tint-subtle border-b-0 bg-tint-subtle theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base"> {title} </div> ) : null} @@ -50,8 +50,8 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( /> <pre className={tcls( - 'hide-scroll relative overflow-auto bg-tint theme-gradient:bg-tint-12/1 ring-tint-subtle [grid-area:2/1]', - 'rounded-md straight-corners:rounded-sm', + 'hide-scroll relative overflow-auto border border-tint-subtle bg-tint-subtle theme-muted:bg-tint-base [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base', + 'rounded-md straight-corners:rounded-sm shadow-sm', title && 'rounded-ss-none' )} > diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css index 22ce3b60e3..21c0fd5791 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css @@ -1,31 +1,67 @@ :root { --shiki-color-text: theme("colors.tint.11"); - --shiki-token-constant: #0a6355; - --shiki-token-string: #8b6d32; - --shiki-token-comment: theme("colors.teal.700/.64"); - --shiki-token-keyword: theme("colors.pomegranate.600"); - --shiki-token-parameter: #0a3069; - --shiki-token-function: #8250df; - --shiki-token-string-expression: #6a4906; - --shiki-token-punctuation: theme("colors.pomegranate.700/.92"); - --shiki-token-link: theme("colors.tint.12"); - --shiki-token-inserted: #22863a; - --shiki-token-deleted: #b31d28; - --shiki-token-changed: #8250df; + --shiki-token-punctuation: theme("colors.tint.11"); + --shiki-token-comment: theme("colors.neutral.9/.7"); + --shiki-token-link: theme("colors.primary.10"); + + --shiki-token-constant: theme("colors.warning.10"); + --shiki-token-string: theme("colors.success.10"); + --shiki-token-string-expression: theme("colors.success.10"); + --shiki-token-keyword: theme("colors.danger.10"); + --shiki-token-parameter: theme("colors.warning.10"); + --shiki-token-function: theme("colors.primary.10"); + + --shiki-token-inserted: theme("colors.success.10"); + --shiki-token-deleted: theme("colors.danger.10"); + --shiki-token-changed: theme("colors.tint.12"); +} + +@media (prefers-contrast: more) { + :root { + --shiki-color-text: theme("colors.tint.12"); + --shiki-token-punctuation: theme("colors.tint.12"); + --shiki-token-comment: theme("colors.neutral.11"); + --shiki-token-link: theme("colors.primary.11"); + + --shiki-token-constant: theme("colors.warning.11"); + --shiki-token-string: theme("colors.success.11"); + --shiki-token-string-expression: theme("colors.success.11"); + --shiki-token-keyword: theme("colors.danger.11"); + --shiki-token-parameter: theme("colors.warning.11"); + --shiki-token-function: theme("colors.primary.11"); + + --shiki-token-inserted: theme("colors.success.11"); + --shiki-token-deleted: theme("colors.danger.11"); + --shiki-token-changed: theme("colors.tint.12"); + } } html.dark { - --shiki-color-text: theme("colors.tint.11"); - --shiki-token-constant: #d19a66; - --shiki-token-string: theme("colors.pomegranate.300"); - --shiki-token-comment: theme("colors.teal.300/.64"); - --shiki-token-keyword: theme("colors.pomegranate.400"); - --shiki-token-parameter: theme("colors.yellow.500"); - --shiki-token-function: #56b6c2; - --shiki-token-string-expression: theme("colors.tint.11"); - --shiki-token-punctuation: #acc6ee; - --shiki-token-link: theme("colors.pomegranate.400"); - --shiki-token-inserted: #85e89d; - --shiki-token-deleted: #fdaeb7; - --shiki-token-changed: #56b6c2; + /* Override select colors to have more contrast */ + --shiki-token-comment: theme("colors.neutral.9"); + + --shiki-token-constant: theme("colors.warning.11"); + --shiki-token-string: theme("colors.success.11"); + --shiki-token-string-expression: theme("colors.success.11"); + --shiki-token-keyword: theme("colors.danger.11"); + --shiki-token-parameter: theme("colors.warning.11"); + --shiki-token-function: theme("colors.primary.11"); +} + +.code-monochrome { + --shiki-token-constant: theme("colors.tint.11"); + --shiki-token-string: theme("colors.tint.12"); + --shiki-token-string-expression: theme("colors.tint.12"); + --shiki-token-keyword: theme("colors.primary.10"); + --shiki-token-parameter: theme("colors.tint.9"); + --shiki-token-function: theme("colors.primary.9"); +} + +html.dark.code-monochrome { + --shiki-token-constant: theme("colors.tint.11"); + --shiki-token-string: theme("colors.tint.12"); + --shiki-token-string-expression: theme("colors.tint.12"); + --shiki-token-keyword: theme("colors.primary.11"); + --shiki-token-parameter: theme("colors.tint.10"); + --shiki-token-function: theme("colors.primary.10"); } diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 39cccc4b38..7d08d97f59 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -169,41 +169,18 @@ @apply flex flex-col; } -.openapi-schema { +.openapi-schema, +.openapi-disclosure { @apply py-2.5 flex flex-col gap-2; } -.openapi-section-body .openapi-schema-properties { - @apply divide-y divide-tint-subtle; -} - -.openapi-disclosure-group-panel > .openapi-schema-properties > *:first-child > .openapi-schema { - @apply pt-0; -} - -.openapi-responsebody > .openapi-schema-properties > .openapi-schema:last-child { - @apply pb-0; -} - -.openapi-responsebody > .openapi-schema-properties > .openapi-schema:only-child { - @apply py-0; -} - .openapi-schema-properties .openapi-schema:last-child { @apply border-b-0; } -.openapi-schema-properties .openapi-schema-opened { - @apply pb-3; -} - -.openapi-schema > .openapi-schema-properties { - @apply mt-3; -} - /* Schema Presentation */ .openapi-schema-presentation { - @apply flex flex-col gap-1.5 font-normal; + @apply flex flex-col gap-1 font-normal; } .openapi-schema-properties:last-child { @@ -221,7 +198,7 @@ } .openapi-schema-propertyname { - @apply select-all font-mono font-normal text-tint-strong; + @apply select-all font-mono font-semibold text-tint-strong; } .openapi-schema-propertyname[data-deprecated="true"] { @@ -244,6 +221,10 @@ @apply text-success dark:text-success-subtle/9 text-[0.813rem] lowercase; } +.openapi-schema-types { + @apply flex items-baseline flex-wrap gap-1; +} + .openapi-schema-type { @apply text-tint select-text text-[0.813rem] font-mono [word-spacing:-0.25rem]; } @@ -321,7 +302,7 @@ .openapi-schema-pattern code, .openapi-schema-enum-value code, .openapi-schema-default code { - @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none; + @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none; } /* Authentication */ @@ -373,28 +354,25 @@ } .openapi-response-tab-content { - @apply overflow-hidden max-w-full flex items-baseline; - @apply text-left text-pretty relative leading-[1.125rem] text-tint !font-normal truncate select-text; + @apply flex items-baseline truncate grow shrink max-w-max basis-[60%] mr-auto; + @apply text-left text-pretty relative leading-tight text-tint select-text; } .openapi-response-description.openapi-markdown { - @apply text-left prose-sm text-[0.813rem] text-pretty h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit; + @apply text-left truncate prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit; } -.openapi-response-description.openapi-markdown::-webkit-scrollbar { - display: none; +.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-tab-content { + @apply basis-full; } -.openapi-response-description p { - @apply truncate max-w-full inline pr-1; -} - -.openapi-response-content-type { - @apply text-xs text-tint-8 ml-auto shrink-0; +.openapi-disclosure-group-trigger[aria-expanded="true"] + .openapi-response-description.openapi-markdown { + @apply whitespace-normal; } .openapi-response-body { - @apply flex flex-col gap-3; + @apply flex flex-col; } /* Response Body and Headers */ @@ -411,16 +389,7 @@ @apply px-3 py-1; } -.openapi-responsebody-header-content, -.openapi-responseheaders-header-content { - /* unstyled */ -} - /* Code Sample */ -.openapi-codesample { - @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle; -} - .openapi-codesample-header { @apply flex flex-row items-center; } @@ -429,6 +398,12 @@ @apply flex flex-row items-center gap-2.5; } +.openapi-panel-heading, +.openapi-codesample-header, +.openapi-response-examples-header { + @apply border-b border-tint-subtle; +} + .openapi-response-examples-header .openapi-select > button { @apply max-w-full overflow-hidden shrink pl-0.5 py-0.5; } @@ -503,8 +478,16 @@ } /* Panel */ -.openapi-panel { - @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle; +.openapi-panel, +.openapi-codesample, +.openapi-response-examples { + @apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm; +} + +.openapi-panel pre, +.openapi-codesample pre, +.openapi-response-examples pre { + @apply bg-transparent border-none rounded-none shadow-none; } .openapi-panel-heading { @@ -513,12 +496,11 @@ .openapi-panel-body { @apply relative; - @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10; } .openapi-panel-footer, .openapi-codesample-footer { - @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint; + @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden; } .openapi-panel-footer .openapi-markdown { @@ -526,10 +508,6 @@ } /* Example */ -.openapi-response-examples { - @apply border rounded-md straight-corners:rounded-none bg-tint border-tint-subtle; -} - .openapi-response-examples-header { @apply flex flex-row items-center p-2.5; } @@ -551,7 +529,6 @@ .openapi-response-examples-panel, .openapi-codesample-panel { @apply flex-1 text-sm relative focus-visible:outline-none; - @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10; } .openapi-example-empty { @@ -703,15 +680,19 @@ body:has(.openapi-select-popover) { /* Disclosure group */ .openapi-disclosure-group { - @apply border-tint-subtle border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative; + @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative; } -.openapi-disclosure-group-header { - @apply flex flex-row items-baseline justify-between gap-3 relative; +.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) { + @apply bg-tint-subtle; } .openapi-disclosure-group-trigger { - @apply flex items-baseline transition-all hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1; + @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1; +} + +.openapi-disclosure-group-label { + @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate; } .openapi-disclosure-group-trigger:disabled { @@ -722,10 +703,6 @@ body:has(.openapi-select-popover) { @apply invisible; } -.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-description { - @apply whitespace-normal; -} - .openapi-disclosure-group-icon > svg { @apply size-3 text-tint-subtle transition-all duration-300; } @@ -735,29 +712,20 @@ body:has(.openapi-select-popover) { } .openapi-disclosure-group-panel { - @apply p-3 pt-1 transition-all; + @apply px-3 transition-all; } .openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg { @apply rotate-90; } -.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype, -.openapi-disclosure-group-mediatype:has(> .openapi-select[data-open="true"]) { - @apply opacity-11 visible flex; -} - .openapi-disclosure-group-mediatype { - @apply opacity-0 invisible text-xs transition-opacity duration-300 shrink-0 absolute right-2.5 top-2.5; -} - -.openapi-disclosure-group-mediatype > span { - @apply px-1 bg-tint-6 text-tint-12 rounded-full straight-corners:rounded-md; + @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint; } /* Disclosure */ .openapi-schemas-disclosure > .openapi-disclosure-trigger { - @apply flex items-center font-mono !w-full transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1; + @apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1; } .openapi-schemas-disclosure > .openapi-disclosure-trigger, @@ -765,49 +733,90 @@ body:has(.openapi-select-popover) { @apply straight-corners:!rounded-none; } +.openapi-disclosure-panel { + @apply ml-1.5 pl-3 border-l border-tint-subtle; +} + +.openapi-schema .openapi-schema-properties .openapi-schema { + @apply animate-fadeIn [animation-fill-mode:both]; +} + .openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg { @apply rotate-90; } .openapi-disclosure-trigger { - @apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl straight-corners:rounded border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1; + @apply flex flex-row justify-between flex-wrap relative items-start gap-2 text-left -mx-3 px-3 -my-2.5 py-2.5 pr-10; } -.openapi-disclosure-trigger span { - @apply truncate; +.openapi-disclosure { + @apply -mx-3 px-3 py-2.5 transition-all flex flex-col ring-tint-subtle; } -.openapi-disclosure svg { - @apply size-3 shrink-0 transition-transform duration-300; +.openapi-disclosure:not( + .openapi-disclosure-group .openapi-disclosure, + .openapi-schema-alternatives .openapi-disclosure + ) { + @apply rounded-xl; } -.openapi-disclosure-trigger[aria-expanded="true"] svg { - @apply rotate-45; +.openapi-disclosure:has(> .openapi-disclosure-trigger:hover) { + @apply bg-tint-subtle; } -.openapi-disclosure-trigger[aria-expanded="true"] { - @apply w-full rounded-lg border-b rounded-b-none straight-corners:rounded-b-none; +.openapi-disclosure:has(> .openapi-disclosure-trigger:hover), +.openapi-disclosure[data-expanded="true"] { + @apply ring-1 shadow-sm; } -.openapi-disclosure-trigger[aria-expanded="false"] { - @apply w-auto; +.openapi-disclosure[data-expanded="true"]:not(:first-child) { + @apply mt-2; +} +.openapi-disclosure[data-expanded="true"]:not(:last-child) { + @apply mb-2; } -.openapi-disclosure-panel[aria-hidden="false"] { - @apply border-b border-x border-tint-subtle rounded-b-lg straight-corners:rounded-b; +.openapi-disclosure-trigger-label { + @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs; } -.openapi-disclosure-panel .openapi-schema { - @apply p-2.5; +.openapi-disclosure-trigger-label span { + @apply hidden; +} + +.openapi-disclosure-trigger-label svg { + @apply size-3 shrink-0 transition-transform duration-300 text-tint-subtle; } -.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child, -.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child .openapi-schema-name { - @apply !m-0; +.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label, +.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label { + @apply shadow ring-1 bg-tint-base; } -.openapi-disclosure .openapi-schema-properties .openapi-schema-enum { - @apply pt-0 mt-0; +.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label span, +.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label span { + @apply block animate-fadeIn; +} + +@media (hover: none) { + /* Make button label always visible on non-hover devices like phones */ + .openapi-disclosure-trigger-label { + @apply relative ring-1 bg-tint-base; + } + .openapi-disclosure-trigger-label span { + @apply block; + } + .openapi-disclosure-trigger { + @apply pr-3; + } +} + +.openapi-disclosure-trigger[aria-expanded="true"] svg { + @apply rotate-45; +} + +.openapi-disclosure-trigger[aria-expanded="false"] { + @apply w-auto; } .openapi-section-body.openapi-schema.openapi-schema-root { @@ -819,6 +828,16 @@ body:has(.openapi-select-popover) { @apply p-2.5; } +.openapi-schema-alternatives { + @apply ml-1.5 pl-3 border-l border-tint-subtle; +} +.openapi-schema-alternative { + @apply relative; +} +.openapi-schema-alternative-separator { + @apply p-0.5 tracking-wide leading-none uppercase text-[0.625rem] text-tint-subtle whitespace-nowrap absolute -left-3 -bottom-2.5 -translate-x-1/2 z-10 bg-tint-base border-y border-tint-subtle -rotate-6; +} + .openapi-tooltip { @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px]; } @@ -865,11 +884,11 @@ body:has(.openapi-select-popover) { } @keyframes popover-leave { - 0% { + from { opacity: 1; transform: translateY(0) scale(1); } - 100% { + to { opacity: 0; transform: translateY(4px) scale(0.95); } diff --git a/packages/react-openapi/src/OpenAPIDisclosure.tsx b/packages/react-openapi/src/OpenAPIDisclosure.tsx index d84b4523ba..56e663b44b 100644 --- a/packages/react-openapi/src/OpenAPIDisclosure.tsx +++ b/packages/react-openapi/src/OpenAPIDisclosure.tsx @@ -9,11 +9,12 @@ import { Button, Disclosure, DisclosurePanel } from 'react-aria-components'; */ export function OpenAPIDisclosure(props: { icon: React.ReactNode; + header: React.ReactNode; children: React.ReactNode; label: string | ((isExpanded: boolean) => string); className?: string; }): React.JSX.Element { - const { icon, children, label, className } = props; + const { icon, header, label, children, className } = props; const [isExpanded, setIsExpanded] = useState(false); return ( @@ -31,8 +32,11 @@ export function OpenAPIDisclosure(props: { : 'none', })} > - {icon} - <span>{typeof label === 'function' ? label(isExpanded) : label}</span> + {header} + <div className="openapi-disclosure-trigger-label"> + <span>{typeof label === 'function' ? label(isExpanded) : label}</span> + {icon} + </div> </Button> <DisclosurePanel className="openapi-disclosure-panel"> {isExpanded ? children : null} diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx index 7f2a428d5c..27584cfc67 100644 --- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx +++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx @@ -96,56 +96,57 @@ function DisclosureItem(props: { return ( <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}> - <div className="openapi-disclosure-group-header"> - <button - slot="trigger" - ref={triggerRef} - {...mergeProps(buttonProps, focusProps)} - disabled={isDisabled} - style={{ - outline: isFocusVisible - ? '2px solid rgb(var(--primary-color-500)/0.4)' - : 'none', - }} - className="openapi-disclosure-group-trigger" - > - <div className="openapi-disclosure-group-icon"> - {icon || ( - <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon"> - <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> - </svg> - )} - </div> + <button + slot="trigger" + ref={triggerRef} + {...mergeProps(buttonProps, focusProps)} + disabled={isDisabled} + style={{ + outline: isFocusVisible + ? '2px solid rgb(var(--primary-color-500)/0.4)' + : 'none', + }} + className="openapi-disclosure-group-trigger" + > + <div className="openapi-disclosure-group-icon"> + {icon || ( + <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon"> + <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> + </svg> + )} + </div> + <div className="openapi-disclosure-group-label"> {group.label} - </button> - {group.tabs ? ( - <div - className="openapi-disclosure-group-mediatype" - onClick={(e) => e.stopPropagation()} - > - {group.tabs?.length > 1 ? ( - <OpenAPISelect - icon={selectIcon} - stateKey={selectStateKey} - onSelectionChange={() => { - state.expand(); - }} - items={group.tabs} - placement="bottom end" - > - {group.tabs.map((tab) => ( - <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}> - {tab.label} - </OpenAPISelectItem> - ))} - </OpenAPISelect> - ) : group.tabs[0]?.label ? ( - <span>{group.tabs[0].label}</span> - ) : null} - </div> - ) : null} - </div> + + {group.tabs ? ( + <div + className="openapi-disclosure-group-mediatype" + onClick={(e) => e.stopPropagation()} + > + {group.tabs?.length > 1 ? ( + <OpenAPISelect + icon={selectIcon} + stateKey={selectStateKey} + onSelectionChange={() => { + state.expand(); + }} + items={group.tabs} + placement="bottom end" + > + {group.tabs.map((tab) => ( + <OpenAPISelectItem key={tab.key} id={tab.key} value={tab}> + {tab.label} + </OpenAPISelectItem> + ))} + </OpenAPISelect> + ) : group.tabs[0]?.label ? ( + <span>{group.tabs[0].label}</span> + ) : null} + </div> + ) : null} + </div> + </button> {state.isExpanded && selectedTab && ( <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}> diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx index de744b7589..3c005ec29b 100644 --- a/packages/react-openapi/src/OpenAPIResponse.tsx +++ b/packages/react-openapi/src/OpenAPIResponse.tsx @@ -1,7 +1,9 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; +import { OpenAPISchemaPresentation } from './OpenAPISchema'; import { OpenAPISchemaProperties } from './OpenAPISchemaServer'; import type { OpenAPIClientContext } from './context'; +import { tString } from './translate'; import { parameterToProperty, resolveDescription } from './utils'; /** @@ -27,7 +29,31 @@ export function OpenAPIResponse(props: { return ( <div className="openapi-response-body"> {headers.length > 0 ? ( - <OpenAPIDisclosure icon={context.icons.plus} label="Headers"> + <OpenAPIDisclosure + header={ + <OpenAPISchemaPresentation + context={context} + property={{ + propertyName: tString(context.translation, 'headers'), + schema: { + type: 'object', + }, + required: null, + }} + /> + } + icon={context.icons.plus} + label={(isExpanded) => + tString( + context.translation, + isExpanded ? 'hide' : 'show', + tString( + context.translation, + headers.length === 1 ? 'header' : 'headers' + ) + ) + } + > <OpenAPISchemaProperties properties={headers.map(([name, header]) => parameterToProperty({ name, ...header }) @@ -40,7 +66,13 @@ export function OpenAPIResponse(props: { <div className="openapi-responsebody"> <OpenAPISchemaProperties id={`response-${context.blockKey}`} - properties={[{ schema: mediaType.schema }]} + properties={[ + { + schema: mediaType.schema, + propertyName: tString(context.translation, 'response'), + required: null, + }, + ]} context={context} /> </div> diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index d268b17cf5..8f4c0cd33c 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -4,6 +4,7 @@ import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; import { useId } from 'react'; +import type { ComponentPropsWithoutRef } from 'react'; import clsx from 'clsx'; import { Markdown } from './Markdown'; @@ -19,73 +20,98 @@ import { checkIsReference, resolveDescription, resolveFirstExample } from './uti type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>; export interface OpenAPISchemaPropertyEntry { - propertyName?: string | undefined; - required?: boolean | undefined; + propertyName?: string; + required?: boolean | null; schema: OpenAPIV3.SchemaObject; } /** * Render a property of an OpenAPI schema. */ -function OpenAPISchemaProperty(props: { - property: OpenAPISchemaPropertyEntry; - context: OpenAPIClientContext; - circularRefs: CircularRefsIds; - className?: string; -}) { - const { circularRefs: parentCircularRefs, context, className, property } = props; +function OpenAPISchemaProperty( + props: { + property: OpenAPISchemaPropertyEntry; + context: OpenAPIClientContext; + circularRefs: CircularRefsIds; + className?: string; + } & Omit<ComponentPropsWithoutRef<'div'>, 'property' | 'context' | 'circularRefs' | 'className'> +) { + const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props; const { schema } = property; const id = useId(); - return ( - <div id={id} className={clsx('openapi-schema', className)}> - <OpenAPISchemaPresentation context={context} property={property} /> - {(() => { - const circularRefId = parentCircularRefs.get(schema); - // Avoid recursing infinitely, and instead render a link to the parent schema - if (circularRefId) { - return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />; - } + const circularRefId = parentCircularRefs.get(schema); + // Avoid recursing infinitely, and instead render a link to the parent schema + if (circularRefId) { + return <OpenAPISchemaCircularRef id={circularRefId} schema={schema} />; + } + + const circularRefs = new Map(parentCircularRefs); + circularRefs.set(schema, id); + + const properties = getSchemaProperties(schema); + + const ancestors = new Set(circularRefs.keys()); + const alternatives = getSchemaAlternatives(schema, ancestors); + + const header = <OpenAPISchemaPresentation context={context} property={property} />; + const content = (() => { + if (properties?.length) { + return ( + <OpenAPISchemaProperties + properties={properties} + circularRefs={circularRefs} + context={context} + /> + ); + } - const circularRefs = new Map(parentCircularRefs); - circularRefs.set(schema, id); - - const properties = getSchemaProperties(schema); - if (properties?.length) { - return ( - <OpenAPIDisclosure - icon={context.icons.plus} - label={(isExpanded) => - getDisclosureLabel({ schema, isExpanded, context }) - } - > - <OpenAPISchemaProperties - properties={properties} + if (alternatives) { + return ( + <div className="openapi-schema-alternatives"> + {alternatives.map((alternativeSchema, index) => ( + <div key={index} className="openapi-schema-alternative"> + <OpenAPISchemaAlternative + schema={alternativeSchema} circularRefs={circularRefs} context={context} /> - </OpenAPIDisclosure> - ); - } + {index < alternatives.length - 1 ? ( + <span className="openapi-schema-alternative-separator"> + {(schema.anyOf || schema.oneOf) && + tString(context.translation, 'or')} + {schema.allOf && tString(context.translation, 'and')} + </span> + ) : null} + </div> + ))} + </div> + ); + } - const ancestors = new Set(circularRefs.keys()); - const alternatives = getSchemaAlternatives(schema, ancestors); - - if (alternatives) { - return alternatives.map((schema, index) => ( - <OpenAPISchemaAlternative - key={index} - schema={schema} - circularRefs={circularRefs} - context={context} - /> - )); - } + return null; + })(); + + if (properties?.length) { + return ( + <OpenAPIDisclosure + icon={context.icons.plus} + className={clsx('openapi-schema', className)} + header={header} + label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} + {...rest} + > + {content} + </OpenAPIDisclosure> + ); + } - return null; - })()} + return ( + <div id={id} {...rest} className={clsx('openapi-schema', className)}> + {header} + {content} </div> ); } @@ -115,6 +141,7 @@ function OpenAPISchemaProperties(props: { circularRefs={circularRefs} property={property} context={context} + style={{ animationDelay: `${index * 0.02}s` }} /> ); })} @@ -205,34 +232,26 @@ function OpenAPISchemaAlternative(props: { context: OpenAPIClientContext; }) { const { schema, circularRefs, context } = props; - - const description = resolveDescription(schema); const properties = getSchemaProperties(schema); - return ( - <> - {description ? ( - <Markdown source={description} className="openapi-schema-description" /> - ) : null} - <OpenAPIDisclosure - icon={context.icons.plus} - label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} - > - {properties?.length ? ( - <OpenAPISchemaProperties - properties={properties} - circularRefs={circularRefs} - context={context} - /> - ) : ( - <OpenAPISchemaProperty - property={{ schema }} - circularRefs={circularRefs} - context={context} - /> - )} - </OpenAPIDisclosure> - </> + return properties?.length ? ( + <OpenAPIDisclosure + icon={context.icons.plus} + header={<OpenAPISchemaPresentation property={{ schema }} context={context} />} + label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} + > + <OpenAPISchemaProperties + properties={properties} + circularRefs={circularRefs} + context={context} + /> + </OpenAPIDisclosure> + ) : ( + <OpenAPISchemaProperty + property={{ schema }} + circularRefs={circularRefs} + context={context} + /> ); } @@ -293,7 +312,7 @@ function OpenAPISchemaEnum(props: { return ( <span className="openapi-schema-enum"> - Available options:{' '} + {tString(context.translation, 'possible_values')}:{' '} {enumValues.map((item, index) => ( <span key={index} className="openapi-schema-enum-value"> <OpenAPICopyButton @@ -313,7 +332,7 @@ function OpenAPISchemaEnum(props: { /** * Render the top row of a schema. e.g: name, type, and required status. */ -function OpenAPISchemaPresentation(props: { +export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry; context: OpenAPIClientContext; }) { @@ -612,16 +631,14 @@ function getDisclosureLabel(props: { if (schema.type === 'array' && !!schema.items) { if (schema.items.oneOf) { label = tString(context.translation, 'available_items').toLowerCase(); - } - // Fallback to "child attributes" for enums and objects - else if (schema.items.enum || schema.items.type === 'object') { - label = tString(context.translation, 'child_attributes').toLowerCase(); + } else if (schema.items.enum || schema.items.type === 'object') { + label = tString(context.translation, 'properties').toLowerCase(); } else { label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items); } } else { - label = schema.title || tString(context.translation, 'child_attributes').toLowerCase(); + label = schema.title || tString(context.translation, 'properties').toLowerCase(); } - return `${isExpanded ? tString(context.translation, 'hide') : tString(context.translation, 'show')} ${label}`; + return tString(context.translation, isExpanded ? 'hide' : 'show', label); } diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx index 6e230419cd..d24efe3f70 100644 --- a/packages/react-openapi/src/OpenAPISchemaName.tsx +++ b/packages/react-openapi/src/OpenAPISchemaName.tsx @@ -6,7 +6,7 @@ import { t, tString } from './translate'; interface OpenAPISchemaNameProps { schema?: OpenAPIV3.SchemaObject; propertyName?: string | React.JSX.Element; - required?: boolean; + required?: boolean | null; type?: string; context: OpenAPIClientContext; } @@ -43,7 +43,7 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { {t(context.translation, 'write_only')} </span> ) : null} - {required ? ( + {required === null ? null : required ? ( <span className="openapi-schema-required"> {t(context.translation, 'required')} </span> diff --git a/packages/react-openapi/src/StaticSection.tsx b/packages/react-openapi/src/StaticSection.tsx index ffb3a2bba8..b79b20e617 100644 --- a/packages/react-openapi/src/StaticSection.tsx +++ b/packages/react-openapi/src/StaticSection.tsx @@ -11,7 +11,7 @@ export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) { {...props} className={clsx( 'openapi-section-header', - props.className && `${props.className}-header` + props.className ? `${props.className}-header` : undefined )} /> ); diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index 2283245fcb..eddb893def 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -9,7 +9,7 @@ import { getOpenAPIClientContext, resolveOpenAPIContext, } from '../context'; -import { t } from '../translate'; +import { t, tString } from '../translate'; import { getExampleFromSchema } from '../util/example'; /** @@ -89,7 +89,10 @@ export function OpenAPISchemas(props: { className="openapi-schemas-disclosure" key={name} icon={context.icons.chevronRight} - label={name} + header={name} + label={(isExpanded) => + tString(context.translation, isExpanded ? 'hide' : 'show') + } > <Section className="openapi-section-schemas"> <SectionBody> diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts index 8fc792e8ea..8a5236c587 100644 --- a/packages/react-openapi/src/translations/de.ts +++ b/packages/react-openapi/src/translations/de.ts @@ -18,9 +18,11 @@ export const de = { nullable: 'Nullfähig', body: 'Rumpf', payload: 'Nutzlast', - headers: 'Kopfzeilen', + headers: 'Header', + header: 'Header', authorizations: 'Autorisierungen', responses: 'Antworten', + response: 'Antwort', path_parameters: 'Pfadparameter', query_parameters: 'Abfrageparameter', header_parameters: 'Header-Parameter', @@ -30,8 +32,11 @@ export const de = { success: 'Erfolg', redirect: 'Umleitung', error: 'Fehler', - show: 'Anzeigen', - hide: 'Verstecken', + show: 'Zeige ${1}', + hide: 'Verstecke ${1}', available_items: 'Verfügbare Elemente', - child_attributes: 'Unterattribute', + properties: 'Eigenschaften', + or: 'oder', + and: 'und', + possible_values: 'Mögliche Werte', }; diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts index 61f45e9e3f..4276595d54 100644 --- a/packages/react-openapi/src/translations/en.ts +++ b/packages/react-openapi/src/translations/en.ts @@ -19,8 +19,10 @@ export const en = { body: 'Body', payload: 'Payload', headers: 'Headers', + header: 'Header', authorizations: 'Authorizations', responses: 'Responses', + response: 'Response', path_parameters: 'Path parameters', query_parameters: 'Query parameters', header_parameters: 'Header parameters', @@ -30,8 +32,11 @@ export const en = { success: 'Success', redirect: 'Redirect', error: 'Error', - show: 'Show', - hide: 'Hide', + show: 'Show ${1}', + hide: 'Hide ${1}', available_items: 'Available items', - child_attributes: 'Child attributes', + possible_values: 'Possible values', + properties: 'Properties', + or: 'or', + and: 'and', }; diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts index 5faa36c674..f5ac905f6a 100644 --- a/packages/react-openapi/src/translations/es.ts +++ b/packages/react-openapi/src/translations/es.ts @@ -18,9 +18,11 @@ export const es = { nullable: 'Nulo', body: 'Cuerpo', payload: 'Caga útil', - headers: 'Encabezados', + headers: 'Headers', + header: 'Header', authorizations: 'Autorizaciones', responses: 'Respuestas', + response: 'Respuesta', path_parameters: 'Parámetros de ruta', query_parameters: 'Parámetros de consulta', header_parameters: 'Parámetros de encabezado', @@ -30,8 +32,11 @@ export const es = { success: 'Éxito', redirect: 'Redirección', error: 'Error', - show: 'Mostrar', - hide: 'Ocultar', + show: 'Mostrar ${1}', + hide: 'Ocultar ${1}', available_items: 'Elementos disponibles', - child_attributes: 'Atributos secundarios', + properties: 'Propiedades', + or: 'o', + and: 'y', + possible_values: 'Valores posibles', }; diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts index ccaf05b3d9..b2e563c813 100644 --- a/packages/react-openapi/src/translations/fr.ts +++ b/packages/react-openapi/src/translations/fr.ts @@ -18,20 +18,25 @@ export const fr = { nullable: 'Nullable', body: 'Corps', payload: 'Charge utile', - headers: 'En-têtes', + headers: 'Headers', + header: 'Header', authorizations: 'Autorisations', responses: 'Réponses', + response: 'Réponse', path_parameters: 'Paramètres de chemin', query_parameters: 'Paramètres de requête', - header_parameters: 'Paramètres d’en-tête', + header_parameters: "Paramètres d'en-tête", attributes: 'Attributs', test_it: 'Tester', information: 'Information', success: 'Succès', redirect: 'Redirection', error: 'Erreur', - show: 'Afficher', - hide: 'Masquer', + show: 'Afficher ${1}', + hide: 'Masquer ${1}', available_items: 'Éléments disponibles', - child_attributes: 'Attributs enfants', + properties: 'Propriétés', + or: 'ou', + and: 'et', + possible_values: 'Valeurs possibles', }; diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts index 55b5b2a0a0..e393dd6cbd 100644 --- a/packages/react-openapi/src/translations/ja.ts +++ b/packages/react-openapi/src/translations/ja.ts @@ -19,8 +19,10 @@ export const ja = { body: '本文', payload: 'ペイロード', headers: 'ヘッダー', + header: 'ヘッダー', authorizations: '認可', responses: 'レスポンス', + response: 'レスポンス', path_parameters: 'パスパラメータ', query_parameters: 'クエリパラメータ', header_parameters: 'ヘッダーパラメータ', @@ -30,8 +32,11 @@ export const ja = { success: '成功', redirect: 'リダイレクト', error: 'エラー', - show: '表示', - hide: '非表示', + show: '${1}を表示', + hide: '${1}を非表示', available_items: '利用可能なアイテム', - child_attributes: '子属性', + properties: 'プロパティ', + or: 'または', + and: 'かつ', + possible_values: '可能な値', }; diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts index 5186580c78..34867ccf15 100644 --- a/packages/react-openapi/src/translations/nl.ts +++ b/packages/react-openapi/src/translations/nl.ts @@ -19,8 +19,10 @@ export const nl = { body: 'Body', payload: 'Payload', headers: 'Headers', + header: 'Header', authorizations: 'Autorisaties', responses: 'Reacties', + response: 'Reactie', path_parameters: 'Padparameters', query_parameters: 'Queryparameters', header_parameters: 'Headerparameters', @@ -30,8 +32,11 @@ export const nl = { success: 'Succes', redirect: 'Omleiding', error: 'Fout', - show: 'Toon', - hide: 'Verbergen', + show: 'Toon ${1}', + hide: 'Verberg ${1}', available_items: 'Beschikbare items', - child_attributes: 'Kindattributen', + properties: 'Eigenschappen', + or: 'of', + and: 'en', + possible_values: 'Mogelijke waarden', }; diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts index a4efc3cfe7..2701177932 100644 --- a/packages/react-openapi/src/translations/no.ts +++ b/packages/react-openapi/src/translations/no.ts @@ -18,9 +18,11 @@ export const no = { nullable: 'Kan være null', body: 'Brødtekst', payload: 'Nyttelast', - headers: 'Overskrifter', + headers: 'Headers', + header: 'Header', authorizations: 'Autorisasjoner', responses: 'Responser', + response: 'Respons', path_parameters: 'Sti-parametere', query_parameters: 'Forespørselsparametere', header_parameters: 'Header-parametere', @@ -30,8 +32,11 @@ export const no = { success: 'Suksess', redirect: 'Viderekobling', error: 'Feil', - show: 'Vis', - hide: 'Skjul', + show: 'Vis ${1}', + hide: 'Skjul ${1}', available_items: 'Tilgjengelige elementer', - child_attributes: 'Barneattributter', + properties: 'Egenskaper', + or: 'eller', + and: 'og', + possible_values: 'Mulige verdier', }; diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts index 7c1f86a7c0..00e8ab3c27 100644 --- a/packages/react-openapi/src/translations/pt-br.ts +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -18,9 +18,11 @@ export const pt_br = { nullable: 'Nulo', body: 'Corpo', payload: 'Carga útil', - headers: 'Cabeçalhos', + headers: 'Headers', + header: 'Header', authorizations: 'Autorizações', responses: 'Respostas', + response: 'Resposta', path_parameters: 'Parâmetros de rota', query_parameters: 'Parâmetros de consulta', header_parameters: 'Parâmetros de cabeçalho', @@ -30,8 +32,11 @@ export const pt_br = { success: 'Sucesso', redirect: 'Redirecionamento', error: 'Erro', - show: 'Mostrar', - hide: 'Ocultar', + show: 'Mostrar ${1}', + hide: 'Ocultar ${1}', available_items: 'Itens disponíveis', - child_attributes: 'Atributos filhos', + properties: 'Propriedades', + or: 'ou', + and: 'e', + possible_values: 'Valores possíveis', }; diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts index 414043fd32..f8299d1993 100644 --- a/packages/react-openapi/src/translations/zh.ts +++ b/packages/react-openapi/src/translations/zh.ts @@ -18,9 +18,11 @@ export const zh = { nullable: '可为 null', body: '请求体', payload: '有效载荷', - headers: '头部信息', + headers: '头字段', + header: '头部', authorizations: '授权', responses: '响应', + response: '响应', path_parameters: '路径参数', query_parameters: '查询参数', header_parameters: '头参数', @@ -30,8 +32,11 @@ export const zh = { success: '成功', redirect: '重定向', error: '错误', - show: '显示', - hide: '隐藏', + show: '显示${1}', + hide: '隐藏${1}', available_items: '可用项', - child_attributes: '子属性', + properties: '属性', + or: '或', + and: '和', + possible_values: '可能的值', }; From 3119066728f79addcf0b1bf5ef8743307199c85f Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Thu, 24 Apr 2025 16:54:11 +0100 Subject: [PATCH 008/127] Support for reusable content across spaces. (#3173) --- .changeset/polite-falcons-agree.md | 6 + bun.lock | 4 +- package.json | 2 +- .../DocumentView/Integration/contentkit.tsx | 2 + .../DocumentView/ReusableContent.tsx | 23 +- packages/gitbook/src/fonts/custom.test.ts | 24 ++ packages/gitbook/src/fonts/custom.ts | 6 +- packages/gitbook/src/lib/api.ts | 1 + packages/gitbook/src/lib/references.tsx | 47 ++- packages/gitbook/src/lib/v1.ts | 339 +++++++++++------- 10 files changed, 302 insertions(+), 152 deletions(-) create mode 100644 .changeset/polite-falcons-agree.md diff --git a/.changeset/polite-falcons-agree.md b/.changeset/polite-falcons-agree.md new file mode 100644 index 0000000000..7743ab7095 --- /dev/null +++ b/.changeset/polite-falcons-agree.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": minor +"gitbook": minor +--- + +Add support for reusable content across spaces. diff --git a/bun.lock b/bun.lock index 5476639847..ff44480802 100644 --- a/bun.lock +++ b/bun.lock @@ -259,7 +259,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.111.0", + "@gitbook/api": "0.113.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -624,7 +624,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.111.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E5Pk28kPD4p6XNWdwFM9pgDijdByseIZQqcFK+/hoW5tEZa5Yw/plRKJyN1hmwfPL6SKq6Maf0fbIzTQiVXyQQ=="], + "@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index fba8d79576..00f323e2f4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packageManager": "bun@1.2.8", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.111.0", + "@gitbook/api": "0.113.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx index fb692c6824..155077ca25 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx @@ -20,6 +20,8 @@ export const contentKitServerContext: ContentKitServerContext = { 'link-external': (props) => <Icon icon="arrow-up-right-from-square" {...props} />, eye: (props) => <Icon icon="eye" {...props} />, lock: (props) => <Icon icon="lock" {...props} />, + check: (props) => <Icon icon="check" {...props} />, + 'check-circle': (props) => <Icon icon="check-circle" {...props} />, }, codeBlock: (props) => { return <PlainCodeBlock code={props.code} syntax={props.syntax} />; diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index e95ffc7545..7e2d28d8d2 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -13,15 +13,28 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon throw new Error('Expected a content context to render a reusable content block'); } - const resolved = await resolveContentRef(block.data.ref, context.contentContext); - if (!resolved?.reusableContent?.document) { + const dataFetcher = block.meta?.token + ? context.contentContext.dataFetcher.withToken({ apiToken: block.meta.token }) + : context.contentContext.dataFetcher; + + const resolved = await resolveContentRef(block.data.ref, { + ...context.contentContext, + dataFetcher, + }); + + if (!resolved?.reusableContent) { + return null; + } + + const reusableContent = resolved.reusableContent.revisionReusableContent; + if (!reusableContent.document) { return null; } const document = await getDataOrNull( - context.contentContext.dataFetcher.getDocument({ - spaceId: context.contentContext.space.id, - documentId: resolved.reusableContent.document, + dataFetcher.getDocument({ + spaceId: resolved.reusableContent.space, + documentId: reusableContent.document, }) ); diff --git a/packages/gitbook/src/fonts/custom.test.ts b/packages/gitbook/src/fonts/custom.test.ts index ce2616187e..b430b76241 100644 --- a/packages/gitbook/src/fonts/custom.test.ts +++ b/packages/gitbook/src/fonts/custom.test.ts @@ -28,6 +28,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, multiWeight: { @@ -81,6 +84,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, multiSource: { @@ -99,6 +105,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, missingFormat: { @@ -117,6 +126,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, empty: { @@ -124,6 +136,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { custom: true, fontFamily: 'Empty Font', fontFaces: [], + permissions: { + edit: false, + }, }, specialChars: { @@ -136,6 +151,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }], }, ], + permissions: { + edit: false, + }, }, complex: { @@ -158,6 +176,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, variousURLs: { @@ -174,6 +195,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, }; diff --git a/packages/gitbook/src/fonts/custom.ts b/packages/gitbook/src/fonts/custom.ts index cb31f771ed..84abdf8b27 100644 --- a/packages/gitbook/src/fonts/custom.ts +++ b/packages/gitbook/src/fonts/custom.ts @@ -1,9 +1,9 @@ -import type { CustomizationFontDefinition } from '@gitbook/api'; +import type { CustomizationFontDefinitionInput } from '@gitbook/api'; /** * Define the custom font faces and set the --font-custom to the custom font name */ -export function generateFontFacesCSS(customFont: CustomizationFontDefinition): string { +export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInput): string { const { fontFaces } = customFont; // Generate font face declarations for all weights @@ -45,7 +45,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinition): s /** * Get a list of font sources to preload (only 400 and 700 weights) */ -export function getFontSourcesToPreload(customFont: CustomizationFontDefinition) { +export function getFontSourcesToPreload(customFont: CustomizationFontDefinitionInput) { return customFont.fontFaces.filter( (face): face is typeof face & { weight: 400 | 700 } => face.weight === 400 || face.weight === 700 diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 2bb2039bc3..ca3ce3b1f8 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -267,6 +267,7 @@ export const getPublishedContentByUrl = cache({ const parsed = parseCacheResponse(response); + // biome-ignore lint/suspicious/noConsole: log the ttl of the token console.log( `Parsed ttl: ${parsed.ttl} at ${Date.now()}, for ${'apiToken' in response.data ? response.data.apiToken : '<no-token>'}` ); diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index ab941e43c5..2ef8c48a6a 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -41,8 +41,12 @@ export interface ResolvedContentRef { file?: RevisionFile; /** Page document resolved from the content ref */ page?: RevisionPageDocument; - /** Resolved reusable content, if the ref points to reusable content on a revision. */ - reusableContent?: RevisionReusableContent; + /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */ + reusableContent?: { + revisionReusableContent: RevisionReusableContent; + space: string; + revision: string; + }; /** Resolve OpenAPI spec filesystem. */ openAPIFilesystem?: Filesystem; } @@ -231,21 +235,52 @@ export async function resolveContentRef( } case 'reusable-content': { + // Figure out which space and revision the reusable content is in. + const container: { space: string; revision: string } | null = await (async () => { + // without a space on the content ref, or if the space is the same as the current one, we can use the current revision. + if (!contentRef.space || contentRef.space === context.space.id) { + return { space: context.space.id, revision: revisionId }; + } + + const space = await getDataOrNull( + dataFetcher.getSpace({ + spaceId: contentRef.space, + shareKey: undefined, + }) + ); + + if (!space) { + return null; + } + + return { space: space.id, revision: space.revision }; + })(); + + if (!container) { + return null; + } + const reusableContent = await getDataOrNull( dataFetcher.getReusableContent({ - spaceId: space.id, - revisionId, + spaceId: container.space, + revisionId: container.revision, reusableContentId: contentRef.reusableContent, }) ); + if (!reusableContent) { return null; } + return { - href: getGitBookAppHref(`/s/${space.id}`), + href: getGitBookAppHref(`/s/${container.space}/~/reusable/${reusableContent.id}`), text: reusableContent.title, active: false, - reusableContent, + reusableContent: { + revisionReusableContent: reusableContent, + space: container.space, + revision: container.revision, + }, }; } diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index ea5219816b..eb2accdc24 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types'; import { createImageResizer } from '@v2/lib/images'; import { createLinker } from '@v2/lib/links'; +import { GitBookAPI } from '@gitbook/api'; import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data'; import { headers } from 'next/headers'; import { @@ -30,6 +31,7 @@ import { getUserById, renderIntegrationUi, searchSiteContent, + withAPI as withAPIV1, } from './api'; import { getDynamicCustomizationSettings } from './customization'; import { withLeadingSlash, withTrailingSlash } from './paths'; @@ -58,7 +60,7 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> { return url; }; - const dataFetcher = await getDataFetcherV1(); + const dataFetcher = getDataFetcherV1(); const imageResizer = createImageResizer({ imagesContextId: host, @@ -82,77 +84,121 @@ export async function getV1BaseContext(): Promise<GitBookBaseContext> { * Try not to use this as much as possible, and instead take the data fetcher from the props. * This data fetcher should only be used at the top of the tree. */ -async function getDataFetcherV1(): Promise<GitBookDataFetcher> { +function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher { + let apiClient: GitBookAPI | undefined; + + /** + * Run a function with the correct API client. If an API token is provided, we + * create a new API client with the token. Otherwise, we use the default API client. + */ + async function withAPI<T>(fn: () => Promise<T>): Promise<T> { + // No token override - we can use the default API client. + if (!apiTokenOverride) { + return fn(); + } + + const client = await api(); + + if (!apiClient) { + // New client uses same endpoint and user agent as the default client. + apiClient = new GitBookAPI({ + endpoint: client.client.endpoint, + authToken: apiTokenOverride, + userAgent: client.client.userAgent, + }); + } + + return withAPIV1( + { + client: apiClient, + contextId: client.contextId, + }, + fn + ); + } + const dataFetcher: GitBookDataFetcher = { async api() { - const result = await api(); - return result.client; + return withAPI(async () => { + const result = await api(); + return result.client; + }); }, - withToken() { - // In v1, the token is global and controlled by the middleware. - // We don't need to do anything special here. - return dataFetcher; + withToken({ apiToken }) { + return getDataFetcherV1(apiToken); }, getUserById(userId) { - return wrapDataFetcherError(async () => { - const user = await getUserById(userId); - if (!user) { - throw new DataFetcherError('User not found', 404); - } - - return user; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const user = await getUserById(userId); + if (!user) { + throw new DataFetcherError('User not found', 404); + } + + return user; + }) + ); }, getPublishedContentSite(params) { - return wrapDataFetcherError(async () => { - return getPublishedContentSite(params); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getPublishedContentSite(params); + }) + ); }, getSpace(params) { - return wrapDataFetcherError(async () => { - return getSpace(params.spaceId, params.shareKey); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getSpace(params.spaceId, params.shareKey); + }) + ); }, getChangeRequest(params) { - return wrapDataFetcherError(async () => { - const changeRequest = await getChangeRequest( - params.spaceId, - params.changeRequestId - ); - if (!changeRequest) { - throw new DataFetcherError('Change request not found', 404); - } - - return changeRequest; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const changeRequest = await getChangeRequest( + params.spaceId, + params.changeRequestId + ); + if (!changeRequest) { + throw new DataFetcherError('Change request not found', 404); + } + + return changeRequest; + }) + ); }, getRevision(params) { - return wrapDataFetcherError(async () => { - return getRevision(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevision(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); + }) + ); }, getRevisionFile(params) { - return wrapDataFetcherError(async () => { - const revisionFile = await getRevisionFile( - params.spaceId, - params.revisionId, - params.fileId - ); - if (!revisionFile) { - throw new DataFetcherError('Revision file not found', 404); - } - - return revisionFile; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const revisionFile = await getRevisionFile( + params.spaceId, + params.revisionId, + params.fileId + ); + if (!revisionFile) { + throw new DataFetcherError('Revision file not found', 404); + } + + return revisionFile; + }) + ); }, getRevisionPageMarkdown() { @@ -160,117 +206,140 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> { }, getDocument(params) { - return wrapDataFetcherError(async () => { - const document = await getDocument(params.spaceId, params.documentId); - if (!document) { - throw new DataFetcherError('Document not found', 404); - } - - return document; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const document = await getDocument(params.spaceId, params.documentId); + if (!document) { + throw new DataFetcherError('Document not found', 404); + } + + return document; + }) + ); }, getComputedDocument(params) { - return wrapDataFetcherError(() => { - return getComputedDocument( - params.organizationId, - params.spaceId, - params.source, - params.seed - ); - }); + return withAPI(() => + wrapDataFetcherError(() => { + return getComputedDocument( + params.organizationId, + params.spaceId, + params.source, + params.seed + ); + }) + ); }, getRevisionPages(params) { - return wrapDataFetcherError(async () => { - return getRevisionPages(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevisionPages(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); + }) + ); }, getRevisionPageByPath(params) { - return wrapDataFetcherError(async () => { - const revisionPage = await getRevisionPageByPath( - params.spaceId, - params.revisionId, - params.path - ); - - if (!revisionPage) { - throw new DataFetcherError('Revision page not found', 404); - } - - return revisionPage; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const revisionPage = await getRevisionPageByPath( + params.spaceId, + params.revisionId, + params.path + ); + + if (!revisionPage) { + throw new DataFetcherError('Revision page not found', 404); + } + + return revisionPage; + }) + ); }, getReusableContent(params) { - return wrapDataFetcherError(async () => { - const reusableContent = await getReusableContent( - params.spaceId, - params.revisionId, - params.reusableContentId - ); - - if (!reusableContent) { - throw new DataFetcherError('Reusable content not found', 404); - } - - return reusableContent; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const reusableContent = await getReusableContent( + params.spaceId, + params.revisionId, + params.reusableContentId + ); + + if (!reusableContent) { + throw new DataFetcherError('Reusable content not found', 404); + } + + return reusableContent; + }) + ); }, getLatestOpenAPISpecVersionContent(params) { - return wrapDataFetcherError(async () => { - const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent( - params.organizationId, - params.slug - ); - - if (!openAPISpecVersionContent) { - throw new DataFetcherError('OpenAPI spec version content not found', 404); - } - - return openAPISpecVersionContent; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent( + params.organizationId, + params.slug + ); + + if (!openAPISpecVersionContent) { + throw new DataFetcherError('OpenAPI spec version content not found', 404); + } + + return openAPISpecVersionContent; + }) + ); }, getSiteRedirectBySource(params) { - return wrapDataFetcherError(async () => { - const siteRedirect = await getSiteRedirectBySource(params); - if (!siteRedirect) { - throw new DataFetcherError('Site redirect not found', 404); - } - - return siteRedirect; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const siteRedirect = await getSiteRedirectBySource(params); + if (!siteRedirect) { + throw new DataFetcherError('Site redirect not found', 404); + } + + return siteRedirect; + }) + ); }, getEmbedByUrl(params) { - return wrapDataFetcherError(() => { - return getEmbedByUrlInSpace(params.spaceId, params.url); - }); + return withAPI(() => + wrapDataFetcherError(() => { + return getEmbedByUrlInSpace(params.spaceId, params.url); + }) + ); }, searchSiteContent(params) { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, cacheBust, scope } = params; - const result = await searchSiteContent( - organizationId, - siteId, - query, - scope, - cacheBust - ); - return result.items; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const { organizationId, siteId, query, cacheBust, scope } = params; + const result = await searchSiteContent( + organizationId, + siteId, + query, + scope, + cacheBust + ); + return result.items; + }) + ); }, renderIntegrationUi(params) { - return wrapDataFetcherError(async () => { - const result = await renderIntegrationUi(params.integrationName, params.request); - return result; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const result = await renderIntegrationUi( + params.integrationName, + params.request + ); + return result; + }) + ); }, streamAIResponse() { From 8339e91e2bc532b87eecba3509d35a53b9fae36d Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Fri, 25 Apr 2025 15:28:09 +0100 Subject: [PATCH 009/127] Fix images and other content refs in reusable content across spaces. (#3190) --- .changeset/big-clocks-rush.md | 5 +++++ .../DocumentView/ReusableContent.tsx | 20 +++++++++++++++++-- packages/gitbook/src/lib/references.tsx | 10 +++++----- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 .changeset/big-clocks-rush.md diff --git a/.changeset/big-clocks-rush.md b/.changeset/big-clocks-rush.md new file mode 100644 index 0000000000..665953599f --- /dev/null +++ b/.changeset/big-clocks-rush.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Fix images in reusable content across spaces. diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index 7e2d28d8d2..bbf2d23f70 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api'; import { resolveContentRef } from '@/lib/references'; +import type { GitBookSpaceContext } from '@v2/lib/context'; import { getDataOrNull } from '@v2/lib/data'; import type { BlockProps } from './Block'; import { UnwrappedBlocks } from './Blocks'; @@ -33,7 +34,7 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon const document = await getDataOrNull( dataFetcher.getDocument({ - spaceId: resolved.reusableContent.space, + spaceId: resolved.reusableContent.space.id, documentId: reusableContent.document, }) ); @@ -42,12 +43,27 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon return null; } + // Create a new context for reusable content block, including + // the data fetcher with the token from the block meta and the correct + // space and revision pointers. + const reusableContentContext: GitBookSpaceContext = { + ...context.contentContext, + dataFetcher, + space: resolved.reusableContent.space, + revisionId: resolved.reusableContent.revision, + pages: [], + shareKey: undefined, + }; + return ( <UnwrappedBlocks nodes={document.nodes} document={document} ancestorBlocks={[...ancestorBlocks, block]} - context={context} + context={{ + ...context, + contentContext: reusableContentContext, + }} /> ); } diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 2ef8c48a6a..7248ab2ded 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -44,7 +44,7 @@ export interface ResolvedContentRef { /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */ reusableContent?: { revisionReusableContent: RevisionReusableContent; - space: string; + space: Space; revision: string; }; /** Resolve OpenAPI spec filesystem. */ @@ -236,10 +236,10 @@ export async function resolveContentRef( case 'reusable-content': { // Figure out which space and revision the reusable content is in. - const container: { space: string; revision: string } | null = await (async () => { + const container: { space: Space; revision: string } | null = await (async () => { // without a space on the content ref, or if the space is the same as the current one, we can use the current revision. if (!contentRef.space || contentRef.space === context.space.id) { - return { space: context.space.id, revision: revisionId }; + return { space: context.space, revision: revisionId }; } const space = await getDataOrNull( @@ -253,7 +253,7 @@ export async function resolveContentRef( return null; } - return { space: space.id, revision: space.revision }; + return { space, revision: space.revision }; })(); if (!container) { @@ -262,7 +262,7 @@ export async function resolveContentRef( const reusableContent = await getDataOrNull( dataFetcher.getReusableContent({ - spaceId: container.space, + spaceId: container.space.id, revisionId: container.revision, reusableContentId: contentRef.reusableContent, }) From 81b4a4db535de78a70a944878b039ac0b4d4fb38 Mon Sep 17 00:00:00 2001 From: Johan Preynat <johan.preynat@gmail.com> Date: Mon, 28 Apr 2025 12:07:34 +0200 Subject: [PATCH 010/127] Allow /~gitbook/image and /~gitbook/icon despite querystring parameters for SEO in robots.txt (#3191) --- .vscode/settings.json | 1 + packages/gitbook/src/routes/robots.ts | 29 +++++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b9d15df191..8a86e5d5d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ ], "tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"], "prettier.enable": false, + "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit", diff --git a/packages/gitbook/src/routes/robots.ts b/packages/gitbook/src/routes/robots.ts index 297fc46771..1e1c0a2873 100644 --- a/packages/gitbook/src/routes/robots.ts +++ b/packages/gitbook/src/routes/robots.ts @@ -8,20 +8,23 @@ export async function serveRobotsTxt(context: GitBookSiteContext) { const { linker } = context; const isRoot = checkIsRootSiteContext(context); - const lines = [ - 'User-agent: *', - // Disallow dynamic routes / search queries - 'Disallow: /*?', - ...((await isSiteIndexable(context)) - ? [ - 'Allow: /', - `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`, - ] - : ['Disallow: /']), - ]; - const content = lines.join('\n'); + const isIndexable = await isSiteIndexable(context); - return new Response(content, { + const lines = isIndexable + ? [ + 'User-agent: *', + // Allow image resizing and icon generation routes for favicons and search results + 'Allow: /~gitbook/image?*', + 'Allow: /~gitbook/icon?*', + // Disallow other dynamic routes / search queries + 'Disallow: /*?', + 'Allow: /', + `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`, + ] + : ['User-agent: *', 'Disallow: /']; + + const robotsTxt = lines.join('\n'); + return new Response(robotsTxt, { headers: { 'Content-Type': 'text/plain', }, From 580101d04e0e513996e3de61f1c7a5b0882e9fd8 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:16:58 +0200 Subject: [PATCH 011/127] Schemas disclosure label causing client error (#3192) --- .changeset/sharp-falcons-punch.md | 6 ++++ .../components/DocumentView/OpenAPI/style.css | 7 +++- packages/react-openapi/src/OpenAPISchema.tsx | 28 ++++----------- .../react-openapi/src/getDisclosureLabel.ts | 25 ++++++++++++++ .../src/schemas/OpenAPISchemaItem.tsx | 34 +++++++++++++++++++ .../src/schemas/OpenAPISchemas.tsx | 25 +++++--------- 6 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 .changeset/sharp-falcons-punch.md create mode 100644 packages/react-openapi/src/getDisclosureLabel.ts create mode 100644 packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx diff --git a/.changeset/sharp-falcons-punch.md b/.changeset/sharp-falcons-punch.md new file mode 100644 index 0000000000..fbcb613276 --- /dev/null +++ b/.changeset/sharp-falcons-punch.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': patch +'gitbook': patch +--- + +Fix schemas disclosure label causing client error diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 7d08d97f59..e615f7bb1a 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -755,11 +755,16 @@ body:has(.openapi-select-popover) { .openapi-disclosure:not( .openapi-disclosure-group .openapi-disclosure, - .openapi-schema-alternatives .openapi-disclosure + .openapi-schema-alternatives .openapi-disclosure, + .openapi-schemas-disclosure .openapi-schema.openapi-disclosure ) { @apply rounded-xl; } +.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure { + @apply !rounded-none; +} + .openapi-disclosure:has(> .openapi-disclosure-trigger:hover) { @apply bg-tint-subtle; } diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 8f4c0cd33c..007c335cf8 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -13,6 +13,7 @@ import { OpenAPIDisclosure } from './OpenAPIDisclosure'; import { OpenAPISchemaName } from './OpenAPISchemaName'; import type { OpenAPIClientContext } from './context'; import { retrocycle } from './decycle'; +import { getDisclosureLabel } from './getDisclosureLabel'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { tString } from './translate'; import { checkIsReference, resolveDescription, resolveFirstExample } from './utils'; @@ -606,6 +607,11 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { if (schema.format) { type += ` · ${schema.format}`; } + + // Only add the title if it's an object (no need for the title of a string, number, etc.) + if (type === 'object' && schema.title) { + type += ` · ${schema.title.replaceAll(' ', '')}`; + } } if ('anyOf' in schema) { @@ -620,25 +626,3 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { return type; } - -function getDisclosureLabel(props: { - schema: OpenAPIV3.SchemaObject; - isExpanded: boolean; - context: OpenAPIClientContext; -}) { - const { schema, isExpanded, context } = props; - let label: string; - if (schema.type === 'array' && !!schema.items) { - if (schema.items.oneOf) { - label = tString(context.translation, 'available_items').toLowerCase(); - } else if (schema.items.enum || schema.items.type === 'object') { - label = tString(context.translation, 'properties').toLowerCase(); - } else { - label = schema.items.title ?? schema.title ?? getSchemaTitle(schema.items); - } - } else { - label = schema.title || tString(context.translation, 'properties').toLowerCase(); - } - - return tString(context.translation, isExpanded ? 'hide' : 'show', label); -} diff --git a/packages/react-openapi/src/getDisclosureLabel.ts b/packages/react-openapi/src/getDisclosureLabel.ts new file mode 100644 index 0000000000..814113b910 --- /dev/null +++ b/packages/react-openapi/src/getDisclosureLabel.ts @@ -0,0 +1,25 @@ +'use client'; + +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import type { OpenAPIClientContext } from './context'; +import { tString } from './translate'; + +export function getDisclosureLabel(props: { + schema: OpenAPIV3.SchemaObject; + isExpanded: boolean; + context: OpenAPIClientContext; +}) { + const { schema, isExpanded, context } = props; + let label: string; + if (schema.type === 'array' && !!schema.items) { + if (schema.items.oneOf) { + label = tString(context.translation, 'available_items').toLowerCase(); + } else { + label = tString(context.translation, 'properties').toLowerCase(); + } + } else { + label = tString(context.translation, 'properties').toLowerCase(); + } + + return tString(context.translation, isExpanded ? 'hide' : 'show', label); +} diff --git a/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx new file mode 100644 index 0000000000..86b7d49e8f --- /dev/null +++ b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { SectionBody } from '../StaticSection'; + +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIDisclosure } from '../OpenAPIDisclosure'; +import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; +import { Section } from '../StaticSection'; +import type { OpenAPIClientContext } from '../context'; +import { getDisclosureLabel } from '../getDisclosureLabel'; + +export function OpenAPISchemaItem(props: { + name: string; + schema: OpenAPIV3.SchemaObject; + context: OpenAPIClientContext; +}) { + const { schema, context, name } = props; + + return ( + <OpenAPIDisclosure + className="openapi-schemas-disclosure" + key={name} + icon={context.icons.plus} + header={name} + label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} + > + <Section className="openapi-section-schemas"> + <SectionBody> + <OpenAPIRootSchema schema={schema} context={context} /> + </SectionBody> + </Section> + </OpenAPIDisclosure> + ); +} diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index eddb893def..c0700a530a 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -1,16 +1,16 @@ import type { OpenAPISchema } from '@gitbook/openapi-parser'; import clsx from 'clsx'; -import { OpenAPIDisclosure } from '../OpenAPIDisclosure'; import { OpenAPIExample } from '../OpenAPIExample'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; -import { Section, SectionBody, StaticSection } from '../StaticSection'; +import { StaticSection } from '../StaticSection'; import { type OpenAPIContextInput, getOpenAPIClientContext, resolveOpenAPIContext, } from '../context'; -import { t, tString } from '../translate'; +import { t } from '../translate'; import { getExampleFromSchema } from '../util/example'; +import { OpenAPISchemaItem } from './OpenAPISchemaItem'; /** * OpenAPI Schemas component. @@ -85,21 +85,12 @@ export function OpenAPISchemas(props: { <div className={clsx('openapi-schemas', className)}> {schemas.map(({ name, schema }) => { return ( - <OpenAPIDisclosure - className="openapi-schemas-disclosure" + <OpenAPISchemaItem key={name} - icon={context.icons.chevronRight} - header={name} - label={(isExpanded) => - tString(context.translation, isExpanded ? 'hide' : 'show') - } - > - <Section className="openapi-section-schemas"> - <SectionBody> - <OpenAPIRootSchema schema={schema} context={clientContext} /> - </SectionBody> - </Section> - </OpenAPIDisclosure> + name={name} + context={clientContext} + schema={schema} + /> ); })} </div> From ae5f1abc58beb20e659e3855b1c128565b0525dc Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 29 Apr 2025 18:30:47 +0200 Subject: [PATCH 012/127] Change `Dropdown`s to use Radix's `DropdownMenu` (#3189) --- .changeset/twelve-beers-confess.md | 5 + bun.lock | 113 ++++++++++- packages/gitbook/e2e/internal.spec.ts | 45 +++-- packages/gitbook/package.json | 5 +- .../src/components/Header/Dropdown.tsx | 143 -------------- .../src/components/Header/DropdownMenu.tsx | 176 ++++++++++++++++++ .../src/components/Header/HeaderLink.tsx | 40 ++-- .../src/components/Header/HeaderLinkMore.tsx | 67 +++---- .../src/components/Header/SpacesDropdown.tsx | 56 +++--- .../Header/SpacesDropdownMenuItem.tsx | 2 +- 10 files changed, 398 insertions(+), 254 deletions(-) create mode 100644 .changeset/twelve-beers-confess.md delete mode 100644 packages/gitbook/src/components/Header/Dropdown.tsx create mode 100644 packages/gitbook/src/components/Header/DropdownMenu.tsx diff --git a/.changeset/twelve-beers-confess.md b/.changeset/twelve-beers-confess.md new file mode 100644 index 0000000000..1c70f6880b --- /dev/null +++ b/.changeset/twelve-beers-confess.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Change `Dropdown`s to use Radix's `DropdownMenu` diff --git a/bun.lock b/bun.lock index ff44480802..d720c44b84 100644 --- a/bun.lock +++ b/bun.lock @@ -62,6 +62,7 @@ "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-tooltip": "^1.1.8", @@ -818,11 +819,15 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.12", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q=="], "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wUi01RrTDTOoGtjEPHsxlzPtVzVc3R/AZ5wfh0dyqMAqolhHAHvG5iQjBCTi2AjQqa77FWWbA3kE3RkD+bDMgQ=="], @@ -836,6 +841,8 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="], @@ -844,6 +851,8 @@ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], @@ -3654,24 +3663,88 @@ "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@radix-ui/react-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-menu/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="], + + "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], + + "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], + + "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], + + "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], + "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ=="], + "@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="], + "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="], + "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="], "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], @@ -3684,6 +3757,8 @@ "@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="], + "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], "@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="], @@ -4692,8 +4767,44 @@ "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@radix-ui/react-menu/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/react-remove-scroll/react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "@radix-ui/react-menu/react-remove-scroll/react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "@radix-ui/react-menu/react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@radix-ui/react-menu/react-remove-scroll/use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "@radix-ui/react-menu/react-remove-scroll/use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index e7875cd4e8..d5e49b705f 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -111,24 +111,24 @@ const testCases: TestsCase[] = [ name: 'Customized variant titles are displayed', url: '', run: async (page) => { - const spaceDrowpdown = page + const spaceDropdown = page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); const variantSelectionDropdown = page.locator( - 'css=[data-testid="space-dropdown-button"] + div' + 'css=[data-testid="dropdown-menu"]' ); // the customized space title await expect( - variantSelectionDropdown.getByRole('link', { + variantSelectionDropdown.getByRole('menuitem', { name: 'Multi-Variants', }) ).toBeVisible(); // the NON-customized space title await expect( - variantSelectionDropdown.getByRole('link', { + variantSelectionDropdown.getByRole('menuitem', { name: 'RFCs', }) ).toBeVisible(); @@ -145,14 +145,17 @@ const testCases: TestsCase[] = [ url: 'api-multi-versions/reference/api-reference/pets', screenshot: false, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); @@ -168,14 +171,18 @@ const testCases: TestsCase[] = [ url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets', screenshot: false, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); @@ -205,14 +212,18 @@ const testCases: TestsCase[] = [ return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`; }, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 6f155974f7..5d0939b249 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -27,6 +27,7 @@ "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-tooltip": "^1.1.8", @@ -37,6 +38,7 @@ "assert-never": "^1.2.1", "bun-types": "^1.1.20", "classnames": "^2.5.1", + "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", "js-cookie": "^3.0.5", "jsontoxml": "^1.0.1", @@ -52,6 +54,7 @@ "openapi-types": "^12.1.3", "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", + "partial-json": "^0.1.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.4.1", @@ -70,8 +73,6 @@ "usehooks-ts": "^3.1.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", - "event-iterator": "^2.0.0", - "partial-json": "^0.1.7", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/packages/gitbook/src/components/Header/Dropdown.tsx b/packages/gitbook/src/components/Header/Dropdown.tsx deleted file mode 100644 index 21b626ac2c..0000000000 --- a/packages/gitbook/src/components/Header/Dropdown.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { Icon } from '@gitbook/icons'; -import { type DetailedHTMLProps, type HTMLAttributes, useId } from 'react'; - -import { type ClassValue, tcls } from '@/lib/tailwind'; - -import { Link, type LinkInsightsProps } from '../primitives'; - -export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit< - Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>, - 'ref' ->; - -/** - * Button with a dropdown. - */ -export function Dropdown<E extends HTMLElement>(props: { - /** Content of the button */ - button: (buttonProps: DropdownButtonProps<E>) => React.ReactNode; - /** Content of the dropdown */ - children: React.ReactNode; - /** Custom styles */ - className?: ClassValue; -}) { - const { button, children, className } = props; - const dropdownId = useId(); - - return ( - <div className={tcls('group/dropdown', 'relative flex min-w-0 shrink')}> - {button({ - id: dropdownId, - tabIndex: 0, - 'aria-expanded': true, - 'aria-haspopup': true, - })} - <div - tabIndex={-1} - role="menu" - aria-orientation="vertical" - aria-labelledby={dropdownId} - className={tcls( - 'w-52', - 'max-h-80', - 'flex', - 'absolute', - 'top-full', - 'left-0', - 'origin-top-left', - 'invisible', - 'transition-opacity', - 'duration-1000', - 'group-hover/dropdown:visible', - 'group-focus-within/dropdown:visible', - className - )} - > - <div className="fixed z-50 w-52"> - <div - className={tcls( - 'mt-2', - 'w-full', - 'max-h-80', - 'bg-tint-base', - 'rounded-lg', - 'straight-corners:rounded-sm', - 'p-2', - 'shadow-1xs', - 'overflow-auto', - 'ring-1', - 'ring-tint-subtle', - 'focus:outline-none' - )} - > - {children} - </div> - </div> - </div> - </div> - ); -} - -/** - * Animated chevron to display in the dropdown button. - */ -export function DropdownChevron() { - return ( - <Icon - icon="chevron-down" - className={tcls( - 'shrink-0', - 'opacity-6', - 'size-3', - 'ms-1', - 'transition-all', - 'group-hover/dropdown:opacity-11', - 'group-focus-within/dropdown:rotate-180' - )} - /> - ); -} - -/** - * Group of menu items in a dropdown. - */ -export function DropdownMenu(props: { children: React.ReactNode }) { - const { children } = props; - - return <div className={tcls('flex', 'flex-col', 'gap-1')}>{children}</div>; -} - -/** - * Menu item in a dropdown. - */ -export function DropdownMenuItem( - props: { - href: string | null; - active?: boolean; - className?: ClassValue; - children: React.ReactNode; - } & LinkInsightsProps -) { - const { children, active = false, href, className, insights } = props; - - if (href) { - return ( - <Link - href={href} - insights={insights} - className={tcls( - 'rounded straight-corners:rounded-sm px-3 py-1 text-sm', - active ? 'bg-primary text-primary-strong' : null, - 'hover:bg-tint-hover', - className - )} - > - {children} - </Link> - ); - } - - return ( - <div className={tcls('px-3 py-1 font-medium text-tint text-xs', className)}>{children}</div> - ); -} diff --git a/packages/gitbook/src/components/Header/DropdownMenu.tsx b/packages/gitbook/src/components/Header/DropdownMenu.tsx new file mode 100644 index 0000000000..8f51e4685c --- /dev/null +++ b/packages/gitbook/src/components/Header/DropdownMenu.tsx @@ -0,0 +1,176 @@ +'use client'; + +import { Icon } from '@gitbook/icons'; +import type { DetailedHTMLProps, HTMLAttributes } from 'react'; +import { useState } from 'react'; + +import { type ClassValue, tcls } from '@/lib/tailwind'; + +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { Link, type LinkInsightsProps } from '../primitives'; + +export type DropdownButtonProps<E extends HTMLElement = HTMLElement> = Omit< + Partial<DetailedHTMLProps<HTMLAttributes<E>, E>>, + 'ref' +>; + +/** + * Button with a dropdown. + */ +export function DropdownMenu(props: { + /** Content of the button */ + button: React.ReactNode; + /** Content of the dropdown */ + children: React.ReactNode; + /** Custom styles */ + className?: ClassValue; + /** Open the dropdown on hover */ + openOnHover?: boolean; +}) { + const { button, children, className, openOnHover = false } = props; + const [hovered, setHovered] = useState(false); + const [clicked, setClicked] = useState(false); + + return ( + <RadixDropdownMenu.Root + modal={false} + open={openOnHover ? clicked || hovered : clicked} + onOpenChange={setClicked} + > + <RadixDropdownMenu.Trigger + asChild + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => (openOnHover ? setClicked(!clicked) : null)} + className="group/dropdown" + > + {button} + </RadixDropdownMenu.Trigger> + + <RadixDropdownMenu.Portal> + <RadixDropdownMenu.Content + data-testid="dropdown-menu" + hideWhenDetached + collisionPadding={8} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + align="start" + className="z-40 animate-present pt-2" + > + <div + className={tcls( + 'flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80', + className + )} + > + {children} + </div> + </RadixDropdownMenu.Content> + </RadixDropdownMenu.Portal> + </RadixDropdownMenu.Root> + ); +} + +/** + * Animated chevron to display in the dropdown button. + */ +export function DropdownChevron() { + return ( + <Icon + icon="chevron-down" + className={tcls( + 'shrink-0', + 'opacity-6', + 'size-3', + 'ms-1', + 'transition-all', + 'group-hover/dropdown:opacity-11', + 'group-data-[state=open]/dropdown:opacity-11', + 'group-data-[state=open]/dropdown:rotate-180' + )} + /> + ); +} + +/** + * Button with a chevron for use in dropdowns. + */ +export function DropdownButton(props: { + children: React.ReactNode; + className?: ClassValue; +}) { + const { children, className } = props; + + return ( + <div className={tcls('group/dropdown', 'flex', 'items-center', className)}> + {children} + <DropdownChevron /> + </div> + ); +} + +/** + * Menu item in a dropdown. + */ +export function DropdownMenuItem( + props: { + href: string | null; + active?: boolean; + className?: ClassValue; + children: React.ReactNode; + } & LinkInsightsProps +) { + const { children, active = false, href, className, insights } = props; + + const itemClassName = tcls( + 'rounded straight-corners:rounded-sm px-3 py-1 text-sm', + active + ? 'bg-primary text-primary-strong data-[highlighted]:bg-primary-hover' + : 'data-[highlighted]:bg-tint-hover', + 'focus:outline-none', + className + ); + + if (href) { + return ( + <RadixDropdownMenu.Item asChild> + <Link href={href} insights={insights} className={itemClassName}> + {children} + </Link> + </RadixDropdownMenu.Item> + ); + } + + return ( + <RadixDropdownMenu.Item + className={tcls('px-3 py-1 font-medium text-tint text-xs', className)} + > + {children} + </RadixDropdownMenu.Item> + ); +} + +export function DropdownSubMenu(props: { children: React.ReactNode; label: React.ReactNode }) { + const { children, label } = props; + + return ( + <RadixDropdownMenu.Sub> + <RadixDropdownMenu.SubTrigger className="flex cursor-pointer items-center justify-between rounded straight-corners:rounded-sm px-3 py-1 text-sm focus:outline-none data-[highlighted]:bg-tint-hover"> + {label} + <Icon icon="chevron-right" className="size-3 shrink-0 opacity-6" /> + </RadixDropdownMenu.SubTrigger> + <RadixDropdownMenu.Portal> + <RadixDropdownMenu.SubContent + hideWhenDetached + collisionPadding={8} + className="z-40 animate-present" + > + <div className="flex max-h-80 min-w-40 max-w-[40vw] flex-col gap-1 overflow-auto rounded-lg straight-corners:rounded-sm bg-tint-base p-2 shadow-lg ring-1 ring-tint-subtle sm:min-w-52 sm:max-w-80"> + {children} + </div> + </RadixDropdownMenu.SubContent> + </RadixDropdownMenu.Portal> + </RadixDropdownMenu.Sub> + ); +} diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx index 184537c15d..fcfb28c723 100644 --- a/packages/gitbook/src/components/Header/HeaderLink.tsx +++ b/packages/gitbook/src/components/Header/HeaderLink.tsx @@ -13,12 +13,11 @@ import { tcls } from '@/lib/tailwind'; import { Button, Link } from '../primitives'; import { - Dropdown, type DropdownButtonProps, DropdownChevron, DropdownMenu, DropdownMenuItem, -} from './Dropdown'; +} from './DropdownMenu'; export async function HeaderLink(props: { context: GitBookSiteContext; @@ -33,21 +32,13 @@ export async function HeaderLink(props: { if (link.links && link.links.length > 0) { return ( - <Dropdown + <DropdownMenu className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`} - button={(buttonProps) => { - if (!target || !link.to) { - return ( - <HeaderItemDropdown - {...buttonProps} - headerPreset={headerPreset} - title={link.title} - /> - ); - } - return ( + button={ + !target || !link.to ? ( + <HeaderItemDropdown headerPreset={headerPreset} title={link.title} /> + ) : ( <HeaderLinkNavItem - {...buttonProps} linkTarget={link.to} linkStyle={linkStyle} headerPreset={headerPreset} @@ -55,15 +46,14 @@ export async function HeaderLink(props: { isDropdown href={target?.href} /> - ); - }} + ) + } + openOnHover={true} > - <DropdownMenu> - {link.links.map((subLink, index) => ( - <SubHeaderLink key={index} {...props} link={subLink} /> - ))} - </DropdownMenu> - </Dropdown> + {link.links.map((subLink, index) => ( + <SubHeaderLink key={index} {...props} link={subLink} /> + ))} + </DropdownMenu> ); } @@ -157,10 +147,12 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese 'text-tint', 'links-default:hover:text-primary', + 'links-default:data-[state=open]:text-primary', 'links-default:tint:hover:text-tint-strong', - + 'links-default:tint:data-[state=open]:text-tint-strong', 'underline-offset-2', 'links-accent:hover:underline', + 'links-accent:data-[state=open]:underline', 'links-accent:underline-offset-4', 'links-accent:decoration-primary-subtle', 'links-accent:decoration-[3px]', diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx index 5d27fc9a8d..7954b80ec4 100644 --- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx +++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx @@ -10,7 +10,7 @@ import type React from 'react'; import { resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; -import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown'; +import { DropdownChevron, DropdownMenu, DropdownMenuItem, DropdownSubMenu } from './DropdownMenu'; import styles from './headerLinks.module.css'; /** @@ -23,7 +23,7 @@ export function HeaderLinkMore(props: { }) { const { label, links, context } = props; - const renderButton = () => ( + const renderButton = ( <button type="button" className={tcls( @@ -45,19 +45,18 @@ export function HeaderLinkMore(props: { return ( <div className={`${styles.linkEllipsis} z-20 items-center`}> - <Dropdown + <DropdownMenu button={renderButton} + openOnHover={true} className={tcls( 'max-md:right-0 max-md:left-auto', context.customization.styling.search === 'prominent' && 'right-0 left-auto' )} > - <DropdownMenu> - {links.map((link, index) => ( - <MoreMenuLink key={index} link={link} context={context} /> - ))} - </DropdownMenu> - </Dropdown> + {links.map((link, index) => ( + <MoreMenuLink key={index} link={link} context={context} /> + ))} + </DropdownMenu> </div> ); } @@ -70,32 +69,28 @@ async function MoreMenuLink(props: { const target = link.to ? await resolveContentRef(link.to, context) : null; - return ( - <> - {'links' in link && link.links.length > 0 && ( - <hr className="-mx-2 my-1 border-tint border-t first:hidden" /> - )} - <DropdownMenuItem - href={target?.href ?? null} - insights={ - link.to - ? { - type: 'link_click', - link: { - target: link.to, - position: SiteInsightsLinkPosition.Header, - }, - } - : undefined - } - > - {link.title} - </DropdownMenuItem> - {'links' in link - ? link.links.map((subLink, index) => ( - <MoreMenuLink key={index} {...props} link={subLink} /> - )) - : null} - </> + return 'links' in link && link.links.length > 0 ? ( + <DropdownSubMenu label={link.title}> + {link.links.map((subLink, index) => { + return <MoreMenuLink key={index} {...props} link={subLink} />; + })} + </DropdownSubMenu> + ) : ( + <DropdownMenuItem + href={target?.href ?? null} + insights={ + link.to + ? { + type: 'link_click', + link: { + target: link.to, + position: SiteInsightsLinkPosition.Header, + }, + } + : undefined + } + > + {link.title} + </DropdownMenuItem> ); } diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 39e81e59f4..9728216b74 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -3,7 +3,7 @@ import type { SiteSpace } from '@gitbook/api'; import { tcls } from '@/lib/tailwind'; import type { GitBookSiteContext } from '@v2/lib/context'; -import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown'; +import { DropdownChevron, DropdownMenu } from './DropdownMenu'; import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem'; export function SpacesDropdown(props: { @@ -16,14 +16,13 @@ export function SpacesDropdown(props: { const { linker } = context; return ( - <Dropdown + <DropdownMenu className={tcls( 'group-hover/dropdown:invisible', // Prevent hover from opening the dropdown, as it's annoying in this context 'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered )} - button={(buttonProps) => ( + button={ <div - {...buttonProps} data-testid="space-dropdown-button" className={tcls( 'flex', @@ -40,25 +39,24 @@ export function SpacesDropdown(props: { 'straight-corners:rounded-none', 'bg-tint-base', - 'group-hover/dropdown:bg-tint-base', - 'group-focus-within/dropdown:bg-tint-base', 'text-sm', 'text-tint', - 'group-hover/dropdown:text-tint-strong', - 'group-focus-within/dropdown:text-tint-strong', + 'hover:text-tint-strong', + 'data-[state=open]:text-tint-strong', 'ring-1', 'ring-tint-subtle', - 'group-hover/dropdown:ring-tint-hover', - 'group-focus-within/dropdown:ring-tint-hover', + 'hover:ring-tint-hover', + 'data-[state=open]:ring-tint-hover', 'contrast-more:bg-tint-base', 'contrast-more:ring-1', - 'contrast-more:group-hover/dropdown:ring-2', + 'contrast-more:hover:ring-2', + 'contrast-more:data-[state=open]:ring-2', 'contrast-more:ring-tint', - 'contrast-more:group-hover/dropdown:ring-tint-hover', - 'contrast-more:group-focus-within/dropdown:ring-tint-hover', + 'contrast-more:hover:ring-tint-hover', + 'contrast-more:data-[state=open]:ring-tint-hover', className )} @@ -66,23 +64,21 @@ export function SpacesDropdown(props: { <span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span> <DropdownChevron /> </div> - )} + } > - <DropdownMenu> - {siteSpaces.map((otherSiteSpace, index) => ( - <SpacesDropdownMenuItem - key={`${otherSiteSpace.id}-${index}`} - variantSpace={{ - id: otherSiteSpace.id, - title: otherSiteSpace.title, - url: otherSiteSpace.urls.published - ? linker.toLinkForContent(otherSiteSpace.urls.published) - : otherSiteSpace.space.urls.app, - }} - active={otherSiteSpace.id === siteSpace.id} - /> - ))} - </DropdownMenu> - </Dropdown> + {siteSpaces.map((otherSiteSpace, index) => ( + <SpacesDropdownMenuItem + key={`${otherSiteSpace.id}-${index}`} + variantSpace={{ + id: otherSiteSpace.id, + title: otherSiteSpace.title, + url: otherSiteSpace.urls.published + ? linker.toLinkForContent(otherSiteSpace.urls.published) + : otherSiteSpace.space.urls.app, + }} + active={otherSiteSpace.id === siteSpace.id} + /> + ))} + </DropdownMenu> ); } diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx index 8c3676ff89..70859abfae 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx @@ -4,7 +4,7 @@ import type { Space } from '@gitbook/api'; import { joinPath } from '@/lib/paths'; import { useCurrentPagePath } from '../hooks'; -import { DropdownMenuItem } from './Dropdown'; +import { DropdownMenuItem } from './DropdownMenu'; function useVariantSpaceHref(variantSpaceUrl: string) { const currentPathname = useCurrentPagePath(); From 0e201d589983e6a0d5c03d7298c869ac56d29fec Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 29 Apr 2025 19:29:32 +0200 Subject: [PATCH 013/127] Add border to filled sidebar on gradient theme (#3185) --- .changeset/two-chairs-wait.md | 5 +++++ .../src/components/TableOfContents/TableOfContents.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/two-chairs-wait.md diff --git a/.changeset/two-chairs-wait.md b/.changeset/two-chairs-wait.md new file mode 100644 index 0000000000..6ca7941574 --- /dev/null +++ b/.changeset/two-chairs-wait.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add border to filled sidebar on gradient theme diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 05ee56160c..dcfb287660 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -66,13 +66,14 @@ export function TableOfContents(props: { <div // The actual sidebar, either shown with a filled bg or transparent. className={tcls( 'lg:-ms-5', - 'relative flex flex-grow flex-col overflow-hidden', + 'relative flex flex-grow flex-col overflow-hidden border-tint-subtle', 'sidebar-filled:bg-tint-subtle', 'theme-muted:bg-tint-subtle', '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle', '[html.sidebar-filled.theme-muted_&]:bg-tint-base', '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', + '[html.sidebar-filled.theme-gradient_&]:border', 'page-no-toc:!bg-transparent', 'sidebar-filled:rounded-xl', From c3f6b8c003998d04ad133fd16dae90772137921e Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 29 Apr 2025 19:29:42 +0200 Subject: [PATCH 014/127] Reduce chroma of text layers (#3188) --- .changeset/eight-vans-judge.md | 5 +++++ packages/colors/src/transformations.ts | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .changeset/eight-vans-judge.md diff --git a/.changeset/eight-vans-judge.md b/.changeset/eight-vans-judge.md new file mode 100644 index 0000000000..c432fde1a3 --- /dev/null +++ b/.changeset/eight-vans-judge.md @@ -0,0 +1,5 @@ +--- +"@gitbook/colors": patch +--- + +Update chroma ratio per step diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts index bf7f770c09..90cb4482eb 100644 --- a/packages/colors/src/transformations.ts +++ b/packages/colors/src/transformations.ts @@ -220,7 +220,21 @@ export function colorScale( continue; } - const chromaRatio = index === 8 || index === 9 ? 1 : index * 0.05; + const chromaRatio = (() => { + switch (index) { + // Step 9 and 10 have max chroma, meaning they are fully saturated. + case 8: + case 9: + return 1; + // Step 11 and 12 have a reduced chroma + case 10: + return 0.4; + case 11: + return 0.1; + default: + return index * 0.05; + } + })(); const shade = { L: targetL, // Blend lightness From 89a5816ee41163fc600475345cee8aef1b24d5b9 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 29 Apr 2025 19:29:56 +0200 Subject: [PATCH 015/127] Fix OpenAPI disclosure label ("Show properties") misalignment on mobile (#3193) --- .changeset/young-poets-cry.md | 5 +++++ .../gitbook/src/components/DocumentView/OpenAPI/style.css | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/young-poets-cry.md diff --git a/.changeset/young-poets-cry.md b/.changeset/young-poets-cry.md new file mode 100644 index 0000000000..8757b729b9 --- /dev/null +++ b/.changeset/young-poets-cry.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix OpenAPI disclosure label ("Show properties") misalignment on mobile diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index e615f7bb1a..f0371016ec 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -806,7 +806,7 @@ body:has(.openapi-select-popover) { @media (hover: none) { /* Make button label always visible on non-hover devices like phones */ .openapi-disclosure-trigger-label { - @apply relative ring-1 bg-tint-base; + @apply relative ring-1 bg-tint-base right-0; } .openapi-disclosure-trigger-label span { @apply block; From 2a805cc082610341cf02670374181790957ca424 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 29 Apr 2025 19:30:11 +0200 Subject: [PATCH 016/127] Change OpenAPI schema-optional from `info` to `tint` color (#3194) --- .changeset/rich-eggs-talk.md | 5 +++++ .../gitbook/src/components/DocumentView/OpenAPI/style.css | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/rich-eggs-talk.md diff --git a/.changeset/rich-eggs-talk.md b/.changeset/rich-eggs-talk.md new file mode 100644 index 0000000000..e863378b55 --- /dev/null +++ b/.changeset/rich-eggs-talk.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Change OpenAPI schema-optional from `info` to `tint` color diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index f0371016ec..601885c5d0 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -210,7 +210,7 @@ } .openapi-schema-optional { - @apply text-info-subtle text-[0.813rem] lowercase; + @apply text-tint-subtle text-[0.813rem] lowercase; } .openapi-schema-readonly { From c4ebb3fecd50ee737487a47519d2f3a774ae59e5 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:27:03 +0200 Subject: [PATCH 017/127] Fix openapi-select hover in responses (#3196) --- .changeset/breezy-bugs-dream.md | 5 +++++ .../gitbook/src/components/DocumentView/OpenAPI/style.css | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/breezy-bugs-dream.md diff --git a/.changeset/breezy-bugs-dream.md b/.changeset/breezy-bugs-dream.md new file mode 100644 index 0000000000..69d2022873 --- /dev/null +++ b/.changeset/breezy-bugs-dream.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix openapi-select hover in responses diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 601885c5d0..f4757ca2b6 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -687,6 +687,10 @@ body:has(.openapi-select-popover) { @apply bg-tint-subtle; } +.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover):has(.openapi-select:hover) { + @apply !bg-transparent; +} + .openapi-disclosure-group-trigger { @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1; } @@ -719,7 +723,7 @@ body:has(.openapi-select-popover) { @apply rotate-90; } -.openapi-disclosure-group-mediatype { +.openapi-disclosure-group-mediatype:not(:has(.openapi-select)) { @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint; } From 42ca7e15804753a12d19e9df5dcbc091aba8ec13 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:44:39 +0200 Subject: [PATCH 018/127] Fix openapi CR preview (#3197) --- .changeset/giant-dolphins-approve.md | 6 ++++++ .../gitbook/src/components/DocumentView/OpenAPI/style.css | 6 +++--- packages/react-openapi/src/OpenAPIDisclosureGroup.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 .changeset/giant-dolphins-approve.md diff --git a/.changeset/giant-dolphins-approve.md b/.changeset/giant-dolphins-approve.md new file mode 100644 index 0000000000..0fa9d77174 --- /dev/null +++ b/.changeset/giant-dolphins-approve.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': patch +'gitbook': patch +--- + +Fix openapi CR preview diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index f4757ca2b6..f3c24cb738 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -692,18 +692,18 @@ body:has(.openapi-select-popover) { } .openapi-disclosure-group-trigger { - @apply flex w-full items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1; + @apply flex w-full cursor-pointer items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1; } .openapi-disclosure-group-label { @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate; } -.openapi-disclosure-group-trigger:disabled { +.openapi-disclosure-group-trigger[aria-disabled="true"] { @apply cursor-default hover:bg-inherit; } -.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon { +.openapi-disclosure-group-trigger[aria-disabled="true"] .openapi-disclosure-group-icon { @apply invisible; } diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx index 27584cfc67..ecef441b76 100644 --- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx +++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx @@ -76,7 +76,7 @@ function DisclosureItem(props: { }); const panelRef = useRef<HTMLDivElement | null>(null); - const triggerRef = useRef<HTMLButtonElement | null>(null); + const triggerRef = useRef<HTMLDivElement | null>(null); const isDisabled = groupState?.isDisabled || !group.tabs?.length || false; const { buttonProps: triggerProps, panelProps } = useDisclosure( { @@ -96,11 +96,11 @@ function DisclosureItem(props: { return ( <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}> - <button + <div slot="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} - disabled={isDisabled} + aria-disabled={isDisabled} style={{ outline: isFocusVisible ? '2px solid rgb(var(--primary-color-500)/0.4)' @@ -146,7 +146,7 @@ function DisclosureItem(props: { </div> ) : null} </div> - </button> + </div> {state.isExpanded && selectedTab && ( <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}> From 5a69692f543e7ab7ca6bc364557e85a762d5bdae Mon Sep 17 00:00:00 2001 From: Taran Vohra <taranvohra@outlook.com> Date: Wed, 30 Apr 2025 19:14:09 +0530 Subject: [PATCH 019/127] Temporarily bump getPublishedContentByUrl timeout to 30s on v1 (#3199) --- packages/gitbook/src/lib/api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index ca3ce3b1f8..e04433dc55 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -242,6 +242,7 @@ export const getLatestOpenAPISpecVersionContent = cache({ * Resolve a URL to the content to render. */ export const getPublishedContentByUrl = cache({ + timeout: 30 * 1000, name: 'api.getPublishedContentByUrl.v7', tag: (url) => getCacheTagForURL(url), get: async ( From 20ebecb114bbf49833887e92c7d740a30d0d1202 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Fri, 2 May 2025 11:48:15 +0200 Subject: [PATCH 020/127] Missing top-level required OpenAPI alternatives (#3207) --- .changeset/shy-plums-deny.md | 5 +++ packages/react-openapi/src/OpenAPISchema.tsx | 38 ++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 .changeset/shy-plums-deny.md diff --git a/.changeset/shy-plums-deny.md b/.changeset/shy-plums-deny.md new file mode 100644 index 0000000000..22193b58e1 --- /dev/null +++ b/.changeset/shy-plums-deny.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Missing top-level required OpenAPI alternatives diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 007c335cf8..7841b1c0f0 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -572,6 +572,9 @@ function flattenAlternatives( schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], ancestors: Set<OpenAPIV3.SchemaObject> ): OpenAPIV3.SchemaObject[] { + // Get the parent schema's required fields from the most recent ancestor + const latestAncestor = Array.from(ancestors).pop(); + return schemasOrRefs.reduce<OpenAPIV3.SchemaObject[]>((acc, schemaOrRef) => { if (checkIsReference(schemaOrRef)) { return acc; @@ -580,16 +583,47 @@ function flattenAlternatives( if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) { const schemas = getSchemaAlternatives(schemaOrRef, ancestors); if (schemas) { - acc.push(...schemas); + acc.push( + ...schemas.map((schema) => ({ + ...schema, + required: mergeRequiredFields(schema, latestAncestor), + })) + ); } return acc; } - acc.push(schemaOrRef); + // For direct schemas, handle required fields + const schema = { + ...schemaOrRef, + required: mergeRequiredFields(schemaOrRef, latestAncestor), + }; + + acc.push(schema); return acc; }, []); } +/** + * Merge the required fields of a schema with the required fields of its latest ancestor. + */ +function mergeRequiredFields( + schemaOrRef: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, + latestAncestor: OpenAPIV3.SchemaObject | undefined +) { + if (!schemaOrRef.required && !latestAncestor?.required) { + return undefined; + } + + if (checkIsReference(schemaOrRef)) { + return latestAncestor?.required; + } + + return Array.from( + new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])]) + ); +} + function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { // Otherwise try to infer a nice title let type = 'any'; From f328a41982c85181c884d05632f541060f9f63dc Mon Sep 17 00:00:00 2001 From: Taran Vohra <taranvohra@outlook.com> Date: Fri, 2 May 2025 16:46:31 +0530 Subject: [PATCH 021/127] Revert getPublishedContentByUrl timeout back to 10s (#3208) --- packages/gitbook/src/lib/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index e04433dc55..ca3ce3b1f8 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -242,7 +242,6 @@ export const getLatestOpenAPISpecVersionContent = cache({ * Resolve a URL to the content to render. */ export const getPublishedContentByUrl = cache({ - timeout: 30 * 1000, name: 'api.getPublishedContentByUrl.v7', tag: (url) => getCacheTagForURL(url), get: async ( From f7a34706c7302093f17ebb7bd324923d00a9f5b4 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Fri, 2 May 2025 19:41:05 +0200 Subject: [PATCH 022/127] Allow high-contrast input colors to be used as-is (#3209) --- .changeset/rich-lamps-compare.md | 5 +++++ packages/colors/src/transformations.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/rich-lamps-compare.md diff --git a/.changeset/rich-lamps-compare.md b/.changeset/rich-lamps-compare.md new file mode 100644 index 0000000000..57b026b7b4 --- /dev/null +++ b/.changeset/rich-lamps-compare.md @@ -0,0 +1,5 @@ +--- +"@gitbook/colors": patch +--- + +Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts index 90cb4482eb..8234a460a9 100644 --- a/packages/colors/src/transformations.ts +++ b/packages/colors/src/transformations.ts @@ -214,7 +214,11 @@ export function colorScale( const targetL = foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]); - if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) { + if ( + index === 8 && + !mix && + (darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2) + ) { // Original colour is close enough to target, so let's use the original colour as step 9. result.push(hex); continue; From 04999662dbcf04ebf6632b8acee270eec7013078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 5 May 2025 09:03:28 +0200 Subject: [PATCH 023/127] Use absolute URLs in sitemap.xml when pointing to sitemap-pages.xml (#3210) --- .changeset/real-donkeys-invent.md | 5 +++++ packages/gitbook/src/routes/sitemap.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-donkeys-invent.md diff --git a/.changeset/real-donkeys-invent.md b/.changeset/real-donkeys-invent.md new file mode 100644 index 0000000000..a322af7d72 --- /dev/null +++ b/.changeset/real-donkeys-invent.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix invalid sitemap.xml generated with relative URLs instead of absolute ones diff --git a/packages/gitbook/src/routes/sitemap.ts b/packages/gitbook/src/routes/sitemap.ts index 4a64e12387..a5f5e452fb 100644 --- a/packages/gitbook/src/routes/sitemap.ts +++ b/packages/gitbook/src/routes/sitemap.ts @@ -141,7 +141,7 @@ function getUrlsFromSiteSpaces(context: GitBookSiteContext, siteSpaces: SiteSpac } const url = new URL(siteSpace.urls.published); url.pathname = joinPath(url.pathname, 'sitemap-pages.xml'); - return context.linker.toLinkForContent(url.toString()); + return context.linker.toAbsoluteURL(context.linker.toLinkForContent(url.toString())); }, []); return urls.filter(filterOutNullable); } From 5d504ffa4c320e8968dc2f036493592a80ddf1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 6 May 2025 10:30:55 +0200 Subject: [PATCH 024/127] Fix resolution of links in reusable content (#3212) --- .changeset/moody-maps-chew.md | 5 ++++ .../DocumentView/ReusableContent.tsx | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 .changeset/moody-maps-chew.md diff --git a/.changeset/moody-maps-chew.md b/.changeset/moody-maps-chew.md new file mode 100644 index 0000000000..0c06afc2ad --- /dev/null +++ b/.changeset/moody-maps-chew.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix resolution of links in reusable contents diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index bbf2d23f70..ccb66babd4 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -46,14 +46,21 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon // Create a new context for reusable content block, including // the data fetcher with the token from the block meta and the correct // space and revision pointers. - const reusableContentContext: GitBookSpaceContext = { - ...context.contentContext, - dataFetcher, - space: resolved.reusableContent.space, - revisionId: resolved.reusableContent.revision, - pages: [], - shareKey: undefined, - }; + const reusableContentContext: GitBookSpaceContext = + context.contentContext.space.id === resolved.reusableContent.space.id + ? context.contentContext + : { + ...context.contentContext, + dataFetcher, + space: resolved.reusableContent.space, + revisionId: resolved.reusableContent.revision, + // When the reusable content is in a different space, we don't resolve relative links to pages + // as this space might not be part of the current site. + // In the future, we might expand the logic to look up the space from the list of all spaces in the site + // and adapt the relative links to point to the correct variant. + pages: [], + shareKey: undefined, + }; return ( <UnwrappedBlocks From a3ec2647646bc39cff40f5ec95b89b0f1fdd98bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Tue, 6 May 2025 15:40:56 +0200 Subject: [PATCH 025/127] Fix Python code sample "null vs None" (#3215) --- .changeset/twelve-news-joke.md | 5 +++++ packages/react-openapi/src/code-samples.test.ts | 3 ++- packages/react-openapi/src/code-samples.ts | 5 ++++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/twelve-news-joke.md diff --git a/.changeset/twelve-news-joke.md b/.changeset/twelve-news-joke.md new file mode 100644 index 0000000000..3c0d08681f --- /dev/null +++ b/.changeset/twelve-news-joke.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": patch +--- + +Fix Python code sample "null vs None" diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts index 363baf5525..7fd24bb3f8 100644 --- a/packages/react-openapi/src/code-samples.test.ts +++ b/packages/react-openapi/src/code-samples.test.ts @@ -415,13 +415,14 @@ describe('python code sample generator', () => { key: 'value', truethy: true, falsey: false, + nullish: null, }, }; const output = generator?.generate(input); expect(output).toBe( - 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({"key":"value","truethy":True,"falsey":False})\n)\n\ndata = response.json()' + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()' ); }); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index fe44da11d8..a6c79fd5cf 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -362,12 +362,15 @@ const BodyGenerators = { return '$$__TRUE__$$'; case false: return '$$__FALSE__$$'; + case null: + return '$$__NULL__$$'; default: return value; } }) .replaceAll('"$$__TRUE__$$"', 'True') - .replaceAll('"$$__FALSE__$$"', 'False'); + .replaceAll('"$$__FALSE__$$"', 'False') + .replaceAll('"$$__NULL__$$"', 'None'); } return { body, code, headers }; From 5e975ab95b35ef9e63b049c57e3b0feae174dbb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Tue, 6 May 2025 15:41:15 +0200 Subject: [PATCH 026/127] Fix code highlighting for HTTP (#3214) --- .changeset/lemon-plants-vanish.md | 8 ++++++++ .../src/components/DocumentView/CodeBlock/theme.css | 6 +++--- packages/react-openapi/src/code-samples.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/lemon-plants-vanish.md diff --git a/.changeset/lemon-plants-vanish.md b/.changeset/lemon-plants-vanish.md new file mode 100644 index 0000000000..0887b10bef --- /dev/null +++ b/.changeset/lemon-plants-vanish.md @@ -0,0 +1,8 @@ +--- +"@gitbook/react-openapi": patch +"gitbook-v2": patch +"gitbook": patch +"@gitbook/colors": patch +--- + +Fix code highlighting for HTTP diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css index 21c0fd5791..5e4a73a889 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css @@ -5,7 +5,7 @@ --shiki-token-link: theme("colors.primary.10"); --shiki-token-constant: theme("colors.warning.10"); - --shiki-token-string: theme("colors.success.10"); + --shiki-token-string: theme("colors.warning.10"); --shiki-token-string-expression: theme("colors.success.10"); --shiki-token-keyword: theme("colors.danger.10"); --shiki-token-parameter: theme("colors.warning.10"); @@ -24,7 +24,7 @@ --shiki-token-link: theme("colors.primary.11"); --shiki-token-constant: theme("colors.warning.11"); - --shiki-token-string: theme("colors.success.11"); + --shiki-token-string: theme("colors.warning.11"); --shiki-token-string-expression: theme("colors.success.11"); --shiki-token-keyword: theme("colors.danger.11"); --shiki-token-parameter: theme("colors.warning.11"); @@ -41,7 +41,7 @@ html.dark { --shiki-token-comment: theme("colors.neutral.9"); --shiki-token-constant: theme("colors.warning.11"); - --shiki-token-string: theme("colors.success.11"); + --shiki-token-string: theme("colors.warning.11"); --shiki-token-string-expression: theme("colors.success.11"); --shiki-token-keyword: theme("colors.danger.11"); --shiki-token-parameter: theme("colors.warning.11"); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index a6c79fd5cf..50c6a9204c 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -30,7 +30,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ { id: 'http', label: 'HTTP', - syntax: 'bash', + syntax: 'http', generate: ({ method, url, headers = {}, body }: CodeSampleInput) => { const { host, path } = parseHostAndPath(url); From 3f292065cd4aeded2cd1469fedf76afa5d21d59a Mon Sep 17 00:00:00 2001 From: Taran Vohra <taranvohra@outlook.com> Date: Wed, 7 May 2025 12:42:28 +0530 Subject: [PATCH 027/127] Update site redirect regex for validation (#3216) --- .changeset/tidy-rings-talk.md | 6 ++++++ packages/gitbook/src/components/SitePage/fetch.ts | 6 +++++- packages/gitbook/src/lib/api.ts | 7 ++++++- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/tidy-rings-talk.md diff --git a/.changeset/tidy-rings-talk.md b/.changeset/tidy-rings-talk.md new file mode 100644 index 0000000000..75ac0e7f8d --- /dev/null +++ b/.changeset/tidy-rings-talk.md @@ -0,0 +1,6 @@ +--- +"gitbook": patch +"gitbook-v2": patch +--- + +Update the regex for validating site redirect diff --git a/packages/gitbook/src/components/SitePage/fetch.ts b/packages/gitbook/src/components/SitePage/fetch.ts index 6ca4e55293..732c36072c 100644 --- a/packages/gitbook/src/components/SitePage/fetch.ts +++ b/packages/gitbook/src/components/SitePage/fetch.ts @@ -69,7 +69,11 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams | // If a page still can't be found, we try with the API, in case we have a redirect at site level. const redirectPathname = withLeadingSlash(rawPathname); - if (/^\/[a-zA-Z0-9-_.\/]+[a-zA-Z0-9-_.]$/.test(redirectPathname)) { + if ( + /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test( + redirectPathname + ) + ) { const redirectSources = new Set<string>([ // Test the pathname relative to the root // For example hello/world -> section/variant/hello/world diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index ca3ce3b1f8..310074450d 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -765,7 +765,12 @@ export const getComputedDocument = cache({ * Mimic the validation done on source server-side to reduce API usage. */ function validateSiteRedirectSource(source: string) { - return source.length <= 512 && /^\/[a-zA-Z0-9-_.\\/]+[a-zA-Z0-9-_.]$/.test(source); + return ( + source.length <= 512 && + /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test( + source + ) + ); } /** From 373f18f4519496232ad169b083b3f6978c788036 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Wed, 7 May 2025 15:57:58 +0200 Subject: [PATCH 028/127] Prevent section group popovers from opening on click (#3213) --- .changeset/thirty-donkeys-help.md | 5 +++++ .../src/components/SiteSections/SiteSectionTabs.tsx | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/thirty-donkeys-help.md diff --git a/.changeset/thirty-donkeys-help.md b/.changeset/thirty-donkeys-help.md new file mode 100644 index 0000000000..529b8a9024 --- /dev/null +++ b/.changeset/thirty-donkeys-help.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Prevent section group popovers from opening on click diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index 2833f9f31a..8496845507 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -83,6 +83,12 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) { ) } asChild + onClick={(e) => { + if (value) { + e.preventDefault(); + e.stopPropagation(); + } + }} > <SectionGroupTab isActive={isActive} @@ -180,7 +186,7 @@ const SectionGroupTab = React.forwardRef(function SectionGroupTab( ref={ref} {...rest} className={tcls( - 'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors', + 'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default', isActive ? 'text-primary-subtle' : 'text-tint hover:bg-tint-hover hover:text-tint-strong' From e6ddc0f07fc5e889e8c672c63ad488845f3522b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Thu, 8 May 2025 08:54:28 +0200 Subject: [PATCH 029/127] Fix sitemap URLs (#3220) --- .changeset/healthy-parrots-call.md | 6 ++++++ packages/gitbook-v2/src/lib/links.ts | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/healthy-parrots-call.md diff --git a/.changeset/healthy-parrots-call.md b/.changeset/healthy-parrots-call.md new file mode 100644 index 0000000000..9503e2afb1 --- /dev/null +++ b/.changeset/healthy-parrots-call.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": patch +"gitbook": patch +--- + +Fix URL in sitemap diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts index 857345703c..b84565d1e7 100644 --- a/packages/gitbook-v2/src/lib/links.ts +++ b/packages/gitbook-v2/src/lib/links.ts @@ -93,6 +93,11 @@ export function createLinker( }, toAbsoluteURL(absolutePath: string): string { + // If the path is already a full URL, we return it as is. + if (URL.canParse(absolutePath)) { + return absolutePath; + } + if (!servedOn.host) { return absolutePath; } From 47f01edcb80ef868fb42a72d7e52663a49417c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Thu, 8 May 2025 09:34:04 +0200 Subject: [PATCH 030/127] Fix E2E icons (#3222) --- packages/gitbook/e2e/util.ts | 42 ++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index c2edad6186..78d7badb71 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -345,19 +345,30 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin */ async function waitForIcons(page: Page) { await page.waitForFunction(() => { - const urlStates: Record<string, 'pending' | 'loaded'> = - (window as any).__ICONS_STATES__ || {}; + const urlStates: Record< + string, + { state: 'pending'; uri: null } | { state: 'loaded'; uri: string } + > = (window as any).__ICONS_STATES__ || {}; (window as any).__ICONS_STATES__ = urlStates; + const fetchSvgAsDataUri = async (url: string): Promise<string> => { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.status}`); + } + + const svgText = await response.text(); + const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22'); + + return `data:image/svg+xml;charset=utf-8,${encoded}`; + }; + const loadUrl = (url: string) => { // Mark the URL as pending. - urlStates[url] = 'pending'; - - const img = new Image(); - img.onload = () => { - urlStates[url] = 'loaded'; - }; - img.src = url; + urlStates[url] = { state: 'pending', uri: null }; + fetchSvgAsDataUri(url).then((uri) => { + urlStates[url] = { state: 'loaded', uri }; + }); }; const icons = Array.from(document.querySelectorAll('svg.gb-icon')); @@ -393,18 +404,11 @@ async function waitForIcons(page: Page) { // If the URL is already queued for loading, we return the state. if (urlStates[url]) { - if (urlStates[url] === 'loaded') { + if (urlStates[url].state === 'loaded') { icon.setAttribute('data-argos-state', 'pending'); - const bckMaskImage = icon.style.maskImage; - const bckDisplay = icon.style.display; - icon.style.maskImage = ''; - icon.style.display = 'none'; + icon.style.maskImage = `url("${urlStates[url].uri}")`; requestAnimationFrame(() => { - icon.style.maskImage = bckMaskImage; - icon.style.display = bckDisplay; - requestAnimationFrame(() => { - icon.setAttribute('data-argos-state', 'loaded'); - }); + icon.setAttribute('data-argos-state', 'loaded'); }); return false; } From cb5598dc19cd82752756a73cb520df554ab9343d Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 8 May 2025 11:54:46 +0200 Subject: [PATCH 031/127] Handle invalid OpenAPI Responses (#3223) --- .changeset/smart-feet-argue.md | 5 ++ .../src/OpenAPIResponseExample.tsx | 70 ++++++++++--------- .../react-openapi/src/OpenAPIResponses.tsx | 8 +-- 3 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 .changeset/smart-feet-argue.md diff --git a/.changeset/smart-feet-argue.md b/.changeset/smart-feet-argue.md new file mode 100644 index 0000000000..079dada9b0 --- /dev/null +++ b/.changeset/smart-feet-argue.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Handle invalid OpenAPI Responses diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index 6d1cfdac25..b4b865f505 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -6,8 +6,8 @@ import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent'; import { type OpenAPIContext, getOpenAPIClientContext } from './context'; import type { OpenAPIOperationData, OpenAPIWebhookData } from './types'; import { getExampleFromReference, getExamples } from './util/example'; -import { createStateKey, getStatusCodeDefaultLabel } from './utils'; -import { checkIsReference, resolveDescription } from './utils'; +import { createStateKey, getStatusCodeDefaultLabel, resolveDescription } from './utils'; +import { checkIsReference } from './utils'; /** * Display an example of the response content. @@ -41,45 +41,47 @@ export function OpenAPIResponseExample(props: { return Number(a) - Number(b); }); - const tabs = responses.map(([key, responseObject]) => { - const description = resolveDescription(responseObject); - const label = description ? ( - <Markdown source={description} /> - ) : ( - getStatusCodeDefaultLabel(key, context) - ); + const tabs = responses + .filter(([_, responseObject]) => responseObject && typeof responseObject === 'object') + .map(([key, responseObject]) => { + const description = resolveDescription(responseObject); + const label = description ? ( + <Markdown source={description} /> + ) : ( + getStatusCodeDefaultLabel(key, context) + ); - if (checkIsReference(responseObject)) { - return { - key: key, - label, - statusCode: key, - body: ( - <OpenAPIExample - example={getExampleFromReference(responseObject, context)} - context={context} - syntax="json" - /> - ), - }; - } + if (checkIsReference(responseObject)) { + return { + key: key, + label, + statusCode: key, + body: ( + <OpenAPIExample + example={getExampleFromReference(responseObject, context)} + context={context} + syntax="json" + /> + ), + }; + } + + if (!responseObject.content || Object.keys(responseObject.content).length === 0) { + return { + key: key, + label, + statusCode: key, + body: <OpenAPIEmptyExample context={context} />, + }; + } - if (!responseObject.content || Object.keys(responseObject.content).length === 0) { return { key: key, label, statusCode: key, - body: <OpenAPIEmptyExample context={context} />, + body: <OpenAPIResponse context={context} content={responseObject.content} />, }; - } - - return { - key: key, - label, - statusCode: key, - body: <OpenAPIResponse context={context} content={responseObject.content} />, - }; - }); + }); if (tabs.length === 0) { return null; diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx index efee2b3ce8..c4565aa788 100644 --- a/packages/react-openapi/src/OpenAPIResponses.tsx +++ b/packages/react-openapi/src/OpenAPIResponses.tsx @@ -20,8 +20,9 @@ export function OpenAPIResponses(props: { }) { const { responses, context } = props; - const groups = Object.entries(responses).map( - ([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => { + const groups = Object.entries(responses) + .filter(([_, response]) => response && typeof response === 'object') + .map(([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => { const tabs = (() => { // If there is no content, but there are headers, we need to show the headers if ( @@ -83,8 +84,7 @@ export function OpenAPIResponses(props: { ), tabs, }; - } - ); + }); const state = useResponseExamplesState(context.blockKey, groups[0]?.key); From 7d7806df30d83be6fd21778c8ab19e151f442749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Thu, 8 May 2025 18:57:50 +0200 Subject: [PATCH 032/127] Passthrough SVG images in image resizing (#3224) --- .changeset/two-lions-tickle.md | 6 ++++ .../src/lib/images/checkIsSizableImageURL.ts | 30 +++++++++++-------- .../src/lib/images/createImageResizer.ts | 6 ++-- .../src/lib/images/resizer/resizeImage.ts | 18 ++++++----- packages/gitbook/src/routes/image.ts | 3 +- 5 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 .changeset/two-lions-tickle.md diff --git a/.changeset/two-lions-tickle.md b/.changeset/two-lions-tickle.md new file mode 100644 index 0000000000..b9fa676033 --- /dev/null +++ b/.changeset/two-lions-tickle.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": minor +"gitbook": minor +--- + +Pass SVG images through image resizing without resizing them to serve them from optimal host. diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts index 45754af505..61e3ebbc2b 100644 --- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts @@ -1,4 +1,8 @@ -import { checkIsHttpURL } from '@/lib/urls'; +export enum SizableImageAction { + Resize = 'resize', + Skip = 'skip', + Passthrough = 'passthrough', +} /** * Check if an image URL is resizable. @@ -6,22 +10,24 @@ import { checkIsHttpURL } from '@/lib/urls'; * Skip it for SVGs. * Skip it for GitBook images (to avoid recursion). */ -export function checkIsSizableImageURL(input: string): boolean { +export function checkIsSizableImageURL(input: string): SizableImageAction { if (!URL.canParse(input)) { - return false; - } - - if (input.includes('/~gitbook/image')) { - return false; + return SizableImageAction.Skip; } const parsed = new URL(input); - if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { - return false; + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return SizableImageAction.Skip; } - if (!checkIsHttpURL(parsed)) { - return false; + if (parsed.hostname === 'localhost') { + return SizableImageAction.Skip; + } + if (parsed.pathname.includes('/~gitbook/image')) { + return SizableImageAction.Skip; + } + if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { + return SizableImageAction.Passthrough; } - return true; + return SizableImageAction.Resize; } diff --git a/packages/gitbook-v2/src/lib/images/createImageResizer.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts index 703fdadd62..8507a7aefd 100644 --- a/packages/gitbook-v2/src/lib/images/createImageResizer.ts +++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts @@ -1,7 +1,7 @@ import 'server-only'; import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env'; import type { GitBookLinker } from '../links'; -import { checkIsSizableImageURL } from './checkIsSizableImageURL'; +import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL'; import { getImageSize } from './resizer'; import { type SignatureVersion, generateImageSignature } from './signatures'; import type { ImageResizer } from './types'; @@ -24,7 +24,7 @@ export function createImageResizer({ return { getResizedImageURL: (urlInput) => { - if (!checkIsSizableImageURL(urlInput)) { + if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) { return null; } @@ -64,7 +64,7 @@ export function createImageResizer({ }, getImageSize: async (input, options) => { - if (!checkIsSizableImageURL(input)) { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { return null; } diff --git a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts index 4ed81dcd2b..8e656f3b70 100644 --- a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts +++ b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts @@ -1,7 +1,7 @@ import 'server-only'; import assertNever from 'assert-never'; import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env'; -import { checkIsSizableImageURL } from '../checkIsSizableImageURL'; +import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL'; import { resizeImageWithCDNCgi } from './cdn-cgi'; import { resizeImageWithCFFetch } from './cf-fetch'; import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types'; @@ -13,7 +13,7 @@ export async function getImageSize( input: string, defaultSize: Partial<CloudflareImageOptions> = {} ): Promise<{ width: number; height: number } | null> { - if (!checkIsSizableImageURL(input)) { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { return null; } @@ -48,13 +48,17 @@ export async function resizeImage( signal?: AbortSignal; } ): Promise<Response> { - const parsed = new URL(input); - if (parsed.protocol === 'data:') { - throw new Error('Cannot resize data: URLs'); + const action = checkIsSizableImageURL(input); + if (action === SizableImageAction.Skip) { + throw new Error( + 'Cannot resize this image, this function should have never been called on this url' + ); } - if (parsed.hostname === 'localhost') { - throw new Error('Cannot resize localhost URLs'); + if (action === SizableImageAction.Passthrough) { + return fetch(input, { + signal: options.signal, + }); } switch (GITBOOK_IMAGE_RESIZE_MODE) { diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts index 45d590f311..3bc946ed90 100644 --- a/packages/gitbook/src/routes/image.ts +++ b/packages/gitbook/src/routes/image.ts @@ -2,6 +2,7 @@ import { CURRENT_SIGNATURE_VERSION, type CloudflareImageOptions, type SignatureVersion, + SizableImageAction, checkIsSizableImageURL, isSignatureVersion, parseImageAPIURL, @@ -40,7 +41,7 @@ export async function serveResizedImage( // Check again if the image can be sized, even though we checked when rendering the Image component // Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain // Also prevent infinite redirects. - if (!checkIsSizableImageURL(url)) { + if (checkIsSizableImageURL(url) === SizableImageAction.Skip) { return new Response('Invalid url parameter', { status: 400 }); } From 56c923c0ed66e91b22d48f4cd304030a5497190e Mon Sep 17 00:00:00 2001 From: spastorelli <spastorelli@users.noreply.github.com> Date: Fri, 9 May 2025 12:04:20 +0200 Subject: [PATCH 033/127] Adapt site resolution/lookup to pass visitor unsigned claims in addition to token (#3202) Co-authored-by: taranvohra <taranvohra@outlook.com> --- bun.lock | 32 ++-- package.json | 2 +- packages/cache-tags/package.json | 2 +- packages/gitbook-v2/package.json | 14 +- packages/gitbook-v2/src/lib/data/lookup.ts | 115 ++++++++++----- packages/gitbook-v2/src/middleware.ts | 27 +++- packages/gitbook/e2e/internal.spec.ts | 2 +- packages/gitbook/package.json | 2 +- ...visitor-token.test.ts => visitors.test.ts} | 113 +++++++++++++- .../src/lib/{visitor-token.ts => visitors.ts} | 139 ++++++++++++++++++ packages/gitbook/src/middleware.ts | 24 +-- packages/react-contentkit/package.json | 2 +- 12 files changed, 393 insertions(+), 81 deletions(-) rename packages/gitbook/src/lib/{visitor-token.test.ts => visitors.test.ts} (61%) rename packages/gitbook/src/lib/{visitor-token.ts => visitors.ts} (70%) diff --git a/bun.lock b/bun.lock index d720c44b84..6e27a46b06 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.111.0", + "@gitbook/api": "^0.115.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -51,7 +51,7 @@ "name": "gitbook", "version": "0.11.1", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -143,7 +143,7 @@ "name": "gitbook-v2", "version": "0.2.5", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", @@ -202,7 +202,7 @@ "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -260,7 +260,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.113.0", + "@gitbook/api": "^0.115.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -625,7 +625,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="], + "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], @@ -4077,7 +4077,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="], + "gitbook-v2/next": ["next@15.4.0-canary.26", "", { "dependencies": { "@next/env": "15.4.0-canary.26", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.26", "@next/swc-darwin-x64": "15.4.0-canary.26", "@next/swc-linux-arm64-gnu": "15.4.0-canary.26", "@next/swc-linux-arm64-musl": "15.4.0-canary.26", "@next/swc-linux-x64-gnu": "15.4.0-canary.26", "@next/swc-linux-x64-musl": "15.4.0-canary.26", "@next/swc-win32-arm64-msvc": "15.4.0-canary.26", "@next/swc-win32-x64-msvc": "15.4.0-canary.26", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-0lq0x+H4ewc6vXth3S9shrcK3eYl+4wLXQqdboVwBbJe0ykB3+QbGdXFIEICCZsmbAOaii0ag0tzqD3y/vr3bw=="], "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="], @@ -4969,23 +4969,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="], + "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.26", "", {}, "sha512-+WeMYRfTZWaosbIAjuNESPVjynDz/NKukoR7mF/u3Wuwr40KgScpxD0IuU0T7XbPfprnaInSKAylufFvrXRh+A=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HxtmV8Uoai8Z4wAU1tFWzASogAS+xVVP5Z5frbFu0yQ+1ocb9xQTjNqhiD5xPSAU8pNGWasCod8tlTCBzJzHQg=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-1MLiD1Bj6xSi5MkkQ8IK7A13KZJG9bzoWqdXT/tveVCinmYrl/zY7z/9dgvG+84gAE6uN4BGjp6f3IxRsvYDBA=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-cIVgFgOdMDbnPixR/u3ICW60/HlnDbACCb2O+p9+DJj7s1dsN63Cs9qxc9pDJb7tgL0BFPhYcmGeJfd/bZ4h7w=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-o4YS6E3FD2DpZBDvUai9bPMLcpcNZ3THc2BzysSbZeARPiAQuKoudwPJoCpi2t7vajrvczpxBwTPG2uL05ypEA=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-M2/MFrQcPI7Ul5Fq5AOeoARrT0B9SrGiy7BLnPuE7Iai1+xkhfSsxIMF5JeDm/GfJnzcwA2oSvrOg0e7KKdaCA=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-p5JpQ7k/1LyBzNZglqA8JJm7GRmadPkTyHoWaqMxhiVdcQHGbjwsiNjjAtMNjetNOXxj8ebxjiBsAt+34Ak1IQ=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-FlXIBNOSwnGxxN+HekUfz4Y0n4gPGzqcY3wa3p+5JhzFT7r0oCxMxOdRbs7w8jF5b6uSkWVIQXWFL43F6+8J4g=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "x64" }, "sha512-h9CKrDiEeBof+8IgHStYATYrKVuUt8ggy6429kViWlDbuY6gkuIplf3IRlfpdWAB32I1e4qqUVl/s2xRMgQdqg=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/package.json b/package.json index 00f323e2f4..447997ab82 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packageManager": "bun@1.2.8", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.113.0", + "@gitbook/api": "^0.115.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index 3d25aa8f48..8850dafbd6 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.111.0", + "@gitbook/api": "^0.115.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 50cf96a0d7..f19b6951d8 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,17 +3,17 @@ "version": "0.2.5", "private": true, "dependencies": { + "@gitbook/api": "^0.115.0", + "@gitbook/cache-tags": "workspace:*", + "@sindresorhus/fnv1a": "^3.1.0", + "assert-never": "^1.2.1", + "jwt-decode": "^4.0.0", "next": "canary", "react": "^19.0.0", "react-dom": "^19.0.0", - "@gitbook/api": "*", - "@gitbook/cache-tags": "workspace:*", - "@sindresorhus/fnv1a": "^3.1.0", - "server-only": "^0.0.1", - "warn-once": "^0.1.1", "rison": "^0.1.1", - "jwt-decode": "^4.0.0", - "assert-never": "^1.2.1" + "server-only": "^0.0.1", + "warn-once": "^0.1.1" }, "devDependencies": { "gitbook": "*", diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts index 498f6e8859..4c999bd7a4 100644 --- a/packages/gitbook-v2/src/lib/data/lookup.ts +++ b/packages/gitbook-v2/src/lib/data/lookup.ts @@ -1,55 +1,102 @@ import { race, tryCatch } from '@/lib/async'; import { joinPath, joinPathWithBaseURL } from '@/lib/paths'; import { trace } from '@/lib/tracing'; -import type { PublishedSiteContentLookup } from '@gitbook/api'; +import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api'; import { apiClient } from './api'; import { getExposableError } from './errors'; import type { DataFetcherResponse } from './types'; import { getURLLookupAlternatives, stripURLSearch } from './urls'; +interface LookupPublishedContentByUrlInput { + url: string; + redirectOnError: boolean; + apiToken: string | null; + visitorPayload: SiteVisitorPayload; +} + +/** + * Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint. + * To optimize caching, we try multiple lookup alternatives and return the first one that matches. + */ +export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) { + return lookupPublishedContentByUrl({ + url: input.url, + fetchLookupAPIResult: ({ url, signal }) => { + const api = apiClient({ apiToken: input.apiToken }); + return trace( + { + operation: 'resolvePublishedContentByUrl', + name: url, + }, + () => + tryCatch( + api.urls.resolvePublishedContentByUrl( + { + url, + ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}), + redirectOnError: input.redirectOnError, + }, + { signal } + ) + ) + ); + }, + }); +} + /** - * Lookup a content by its URL using the GitBook API. + * Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint. * To optimize caching, we try multiple lookup alternatives and return the first one that matches. + * + * @deprecated use resolvePublishedContentByUrl. + * */ -export async function getPublishedContentByURL(input: { +export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) { + return lookupPublishedContentByUrl({ + url: input.url, + fetchLookupAPIResult: ({ url, signal }) => { + const api = apiClient({ apiToken: input.apiToken }); + return trace( + { + operation: 'getPublishedContentByURL', + name: url, + }, + () => + tryCatch( + api.urls.getPublishedContentByUrl( + { + url, + visitorAuthToken: input.visitorPayload.jwtToken ?? undefined, + redirectOnError: input.redirectOnError, + // @ts-expect-error - cacheVersion is not a real query param + cacheVersion: 'v2', + }, + { signal } + ) + ) + ); + }, + }); +} + +type TryCatch<T> = ReturnType<typeof tryCatch<T>>; + +async function lookupPublishedContentByUrl(input: { url: string; - visitorAuthToken: string | null; - redirectOnError: boolean; - apiToken: string | null; + fetchLookupAPIResult: (args: { + url: string; + signal: AbortSignal; + }) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>; }): Promise<DataFetcherResponse<PublishedSiteContentLookup>> { const lookupURL = new URL(input.url); const url = stripURLSearch(lookupURL); const lookup = getURLLookupAlternatives(url); const result = await race(lookup.urls, async (alternative, { signal }) => { - const api = await apiClient({ apiToken: input.apiToken }); - - const callResult = await trace( - { - operation: 'getPublishedContentByURL', - name: alternative.url, - }, - () => - tryCatch( - api.urls.getPublishedContentByUrl( - { - url: alternative.url, - visitorAuthToken: input.visitorAuthToken ?? undefined, - redirectOnError: input.redirectOnError, - - // As this endpoint is cached by our API, we version the request - // to void getting stale data with missing properties. - // this could be improved by ensuring our API cache layer is versioned - // or invalidated when needed - // @ts-expect-error - cacheVersion is not a real query param - cacheVersion: 'v2', - }, - { - signal, - } - ) - ) - ); + const callResult = await input.fetchLookupAPIResult({ + url: alternative.url, + signal, + }); if (callResult.error) { if (alternative.primary) { diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index dd983b536a..5cb34052f8 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -10,15 +10,16 @@ import { type ResponseCookies, getPathScopedCookieName, getResponseCookiesForVisitorAuth, - getVisitorToken, + getVisitorPayload, normalizeVisitorAuthURL, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { serveResizedImage } from '@/routes/image'; import { DataFetcherError, getPublishedContentByURL, getVisitorAuthBasePath, normalizeURL, + resolvePublishedContentByUrl, throwIfDataError, } from '@v2/lib/data'; import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env'; @@ -33,6 +34,15 @@ export const config = { type URLWithMode = { url: URL; mode: 'url' | 'url-host' }; +/** + * Temporary list of hosts to test adaptive content using the new resolution API. + */ +const ADAPTIVE_CONTENT_HOSTS = [ + 'docs.gitbook.com', + 'adaptive-docs.gitbook-staging.com', + 'enriched-content-playground.gitbook-staging.io', +]; + export async function middleware(request: NextRequest) { try { const requestURL = new URL(request.url); @@ -85,17 +95,22 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // // Detect and extract the visitor authentication token from the request // - // @ts-ignore - request typing - const visitorToken = getVisitorToken({ + const { visitorToken, unsignedClaims } = getVisitorPayload({ cookies: request.cookies.getAll(), url: siteRequestURL, }); const withAPIToken = async (apiToken: string | null) => { + const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname) + ? resolvePublishedContentByUrl + : getPublishedContentByURL; const siteURLData = await throwIfDataError( - getPublishedContentByURL({ + resolve({ url: siteRequestURL.toString(), - visitorAuthToken: visitorToken?.token ?? null, + visitorPayload: { + jwtToken: visitorToken?.token ?? undefined, + unsignedClaims, + }, // When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie. // For example when the VA backend signature has changed but the token stored in the cookie is not yet expired. diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index d5e49b705f..83181c3c9a 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -12,7 +12,7 @@ import { VISITOR_TOKEN_COOKIE, getVisitorAuthCookieName, getVisitorAuthCookieValue, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { getSiteAPIToken } from '../tests/utils'; import { diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 5d0939b249..4294327546 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" }, "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", diff --git a/packages/gitbook/src/lib/visitor-token.test.ts b/packages/gitbook/src/lib/visitors.test.ts similarity index 61% rename from packages/gitbook/src/lib/visitor-token.test.ts rename to packages/gitbook/src/lib/visitors.test.ts index a1f07d2504..82060fcdfa 100644 --- a/packages/gitbook/src/lib/visitor-token.test.ts +++ b/packages/gitbook/src/lib/visitors.test.ts @@ -6,7 +6,8 @@ import { getVisitorAuthCookieName, getVisitorAuthCookieValue, getVisitorToken, -} from './visitor-token'; + getVisitorUnsignedClaims, +} from './visitors'; describe('getVisitorAuthToken', () => { it('should return the token from the query parameters', () => { @@ -158,3 +159,113 @@ function assertVisitorAuthCookieValue( throw new Error('Expected a VisitorAuthCookieValue'); } + +describe('getVisitorUnsignedClaims', () => { + it('should merge claims from multiple public cookies', () => { + const cookies = [ + { + name: 'gitbook-visitor-public-bucket', + value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }), + }, + { + name: 'gitbook-visitor-public-launchdarkly', + value: JSON.stringify({ + launchdarkly: { flags: { ALPHA: true, API: true } }, + }), + }, + ]; + + const url = new URL('https://example.com/'); + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims).toStrictEqual({ + bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } }, + launchdarkly: { flags: { ALPHA: true, API: true } }, + }); + }); + + it('should parse visitor.* query params with simple types', () => { + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.language=fr&visitor.country=fr' + ); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims).toStrictEqual({ + isEnterprise: true, + language: 'fr', + country: 'fr', + }); + }); + + it('should ignore params that do not match visitor.* convention', () => { + const url = new URL('https://example.com/?visitor.isEnterprise=true&otherParam=true'); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims).toStrictEqual({ + isEnterprise: true, + // otherParam is not present + }); + }); + + it('should support nested query param keys via dot notation', () => { + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false' + ); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims).toStrictEqual({ + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + }); + + it('should ignore invalid JSON in cookie values', () => { + const cookies = [ + { + name: 'gitbook-visitor-public', + value: '{not: "json"}', + }, + ]; + const url = new URL('https://example.com/'); + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims).toStrictEqual({}); + }); + + it('should merge claims from cookies and visitor.* query params', () => { + const cookies = [ + { + name: 'gitbook-visitor-public', + value: JSON.stringify({ role: 'admin', language: 'fr' }), + }, + { + name: 'gitbook-visitor-public-bucket', + value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }), + }, + ]; + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false' + ); + + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims).toStrictEqual({ + role: 'admin', + language: 'fr', + bucket: { + flags: { SITE_AI: true, SITE_PREVIEW: true }, + }, + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + }); +}); diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitors.ts similarity index 70% rename from packages/gitbook/src/lib/visitor-token.ts rename to packages/gitbook/src/lib/visitors.ts index 64f0c97931..c6a7aae986 100644 --- a/packages/gitbook/src/lib/visitor-token.ts +++ b/packages/gitbook/src/lib/visitors.ts @@ -4,6 +4,7 @@ import hash from 'object-hash'; const VISITOR_AUTH_PARAM = 'jwt_token'; export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token'; +const VISITOR_UNSIGNED_CLAIMS_PREFIX = 'gitbook-visitor-public'; /** * Typing for a cookie, matching the internal type of Next.js. @@ -30,6 +31,25 @@ type VisitorAuthCookieValue = { token: string; }; +type ClaimPrimitive = + | string + | number + | boolean + | null + | undefined + | { [key: string]: ClaimPrimitive } + | ClaimPrimitive[]; + +/** + * The result of a visitor info lookup that can include: + * - a visitor token (JWT) + * - a record of visitor public/unsigned claims (JSON object) + */ +export type VisitorPayloadLookup = { + visitorToken: VisitorTokenLookup; + unsignedClaims: Record<string, ClaimPrimitive>; +}; + /** * The result of a visitor token lookup. */ @@ -53,6 +73,25 @@ export type VisitorTokenLookup = /** Not visitor token was found */ | undefined; +/** + * Get the visitor info for the request including its token and/or unsigned claims when present. + */ +export function getVisitorPayload({ + cookies, + url, +}: { + cookies: RequestCookies; + url: URL | NextRequest['nextUrl']; +}): VisitorPayloadLookup { + const visitorToken = getVisitorToken({ cookies, url }); + const unsignedClaims = getVisitorUnsignedClaims({ cookies, url }); + + return { + visitorToken, + unsignedClaims, + }; +} + /** * Get the visitor token for the request. This token can either be in the * query parameters or stored as a cookie. @@ -82,6 +121,106 @@ export function getVisitorToken({ } } +/** + * Get the visitor unsigned/public claims for the request. They can either be in `visitor.` query + * parameters or stored in special `gitbook-visitor-public-*` cookies. + */ +export function getVisitorUnsignedClaims(args: { + cookies: RequestCookies; + url: URL | NextRequest['nextUrl']; +}): Record<string, ClaimPrimitive> { + const { cookies, url } = args; + const claims: Record<string, ClaimPrimitive> = {}; + + for (const cookie of cookies) { + if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) { + try { + const parsed = JSON.parse(cookie.value); + if (typeof parsed === 'object' && parsed !== null) { + Object.assign(claims, parsed); + } + } catch (_err) { + console.warn(`Invalid JSON in unsigned claim cookie "${cookie.name}"`); + } + } + } + + for (const [key, value] of url.searchParams.entries()) { + if (key.startsWith('visitor.')) { + const claimPath = key.substring('visitor.'.length); + const claimValue = parseVisitorQueryParamValue(value); + setVisitorClaimByPath(claims, claimPath, claimValue); + } + } + + return claims; +} + +/** + * Set the value of claims in a claims object at a specific path. + */ +function setVisitorClaimByPath( + claims: Record<string, ClaimPrimitive>, + keyPath: string, + value: ClaimPrimitive +): void { + const keys = keyPath.split('.'); + let current = claims; + + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + + if (index === keys.length - 1) { + current[key] = value; + } else { + if (!(key in current) || !isClaimPrimitiveObject(current[key])) { + current[key] = {}; + } + + current = current[key]; + } + } +} + +function isClaimPrimitiveObject(value: unknown): value is Record<string, ClaimPrimitive> { + return typeof value === 'object' && value !== null; +} + +/** + * Parse the value expected in a `visitor.` URL query parameter. + */ +function parseVisitorQueryParamValue(value: string): ClaimPrimitive { + if (value === 'true') { + return true; + } + + if (value === 'false') { + return false; + } + + if (value === 'null') { + return null; + } + + if (value === 'undefined') { + return undefined; + } + + const num = Number(value); + if (!Number.isNaN(num) && value.trim() !== '') { + return num; + } + + try { + const parsed = JSON.parse(value); + if (typeof parsed === 'object' && parsed !== null) { + return parsed; + } + } catch {} + + return value; +} + /** * Return the lookup result for content served with visitor auth. */ diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 30f7b44e6a..04b1b40dd9 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -24,9 +24,9 @@ import { type ResponseCookies, type VisitorTokenLookup, getResponseCookiesForVisitorAuth, - getVisitorToken, + getVisitorPayload, normalizeVisitorAuthURL, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { joinPath, withLeadingSlash } from '@/lib/paths'; import { getProxyModeBasePath } from '@/lib/proxy'; @@ -392,17 +392,17 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup * When serving multi spaces based on the current URL. */ async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> { - const visitorAuthToken = getVisitorToken({ + const { visitorToken } = getVisitorPayload({ cookies: request.cookies.getAll(), url, }); - const lookup = await lookupSiteByAPI(url, visitorAuthToken); + const lookup = await lookupSiteByAPI(url, visitorToken); return { ...lookup, - ...('basePath' in lookup && visitorAuthToken - ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken) + ...('basePath' in lookup && visitorToken + ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken) : {}), - visitorToken: visitorAuthToken?.token, + visitorToken: visitorToken?.token, }; } @@ -609,12 +609,12 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis const target = new URL(targetStr); target.search = url.search; - const visitorAuthToken = getVisitorToken({ + const { visitorToken } = getVisitorPayload({ cookies: request.cookies.getAll(), url: target, }); - const lookup = await lookupSiteByAPI(target, visitorAuthToken); + const lookup = await lookupSiteByAPI(target, visitorToken); if ('error' in lookup) { return lookup; } @@ -641,10 +641,10 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis ...lookup, siteBasePath: joinPath(target.host, lookup.siteBasePath), basePath: joinPath(target.host, lookup.basePath), - ...('basePath' in lookup && visitorAuthToken - ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken) + ...('basePath' in lookup && visitorToken + ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken) : {}), - visitorToken: visitorAuthToken?.token, + visitorToken: visitorToken?.token, }; } diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index d2c8bd8ee7..09e6cc8e7c 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/icons": "workspace:*" }, "peerDependencies": { From 0c973a32925d6cc58c172d44f02c8656264bbf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Fri, 9 May 2025 12:31:52 +0200 Subject: [PATCH 034/127] Always link main logo to the root of the site (#3226) --- .changeset/tough-starfishes-grin.md | 5 +++++ packages/gitbook/src/components/Header/HeaderLogo.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tough-starfishes-grin.md diff --git a/.changeset/tough-starfishes-grin.md b/.changeset/tough-starfishes-grin.md new file mode 100644 index 0000000000..2f150b59d8 --- /dev/null +++ b/.changeset/tough-starfishes-grin.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Always link main logo to the root of the site diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx index 08a1d2d10a..64c159472f 100644 --- a/packages/gitbook/src/components/Header/HeaderLogo.tsx +++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx @@ -20,7 +20,7 @@ export async function HeaderLogo(props: HeaderLogoProps) { return ( <Link - href={linker.toAbsoluteURL(linker.toPathInSpace(''))} + href={linker.toAbsoluteURL(linker.toPathInSite(''))} className={tcls('group/headerlogo', 'min-w-0', 'shrink', 'flex', 'items-center')} > {customization.header.logo ? ( From aed79fd6c36a088e5bbad58f1f63bdb6fc8b37b8 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Fri, 9 May 2025 14:45:38 +0200 Subject: [PATCH 035/127] Remove header logo rounding (#3225) --- .changeset/fifty-pots-work.md | 5 +++++ packages/gitbook/src/components/Header/HeaderLogo.tsx | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 .changeset/fifty-pots-work.md diff --git a/.changeset/fifty-pots-work.md b/.changeset/fifty-pots-work.md new file mode 100644 index 0000000000..0ba6550278 --- /dev/null +++ b/.changeset/fifty-pots-work.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Decrease rounding of header logo diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx index 64c159472f..72d0b4798b 100644 --- a/packages/gitbook/src/components/Header/HeaderLogo.tsx +++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx @@ -48,8 +48,6 @@ export async function HeaderLogo(props: HeaderLogoProps) { ]} priority="high" style={tcls( - 'rounded', - 'straight-corners:rounded-sm', 'overflow-hidden', 'shrink', 'min-w-0', From e15757d01fec3ea006b253d580a6d06115da1b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 9 May 2025 15:17:49 +0200 Subject: [PATCH 036/127] Use next@15.3.2 instead of canary (#3227) --- .changeset/modern-windows-type.md | 5 +++++ .github/workflows/deploy-preview.yaml | 2 -- bun.lock | 22 +++++++++++----------- packages/gitbook-v2/package.json | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 .changeset/modern-windows-type.md diff --git a/.changeset/modern-windows-type.md b/.changeset/modern-windows-type.md new file mode 100644 index 0000000000..d2135d1c28 --- /dev/null +++ b/.changeset/modern-windows-type.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Fix crash on Cloudflare by using latest stable version of Next.js instead of canary diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index 156c1cfecc..4b14e3f83d 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -200,7 +200,6 @@ jobs: name: Visual Testing v2 (Cloudflare) needs: deploy-v2-cloudflare timeout-minutes: 10 - if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4 @@ -263,7 +262,6 @@ jobs: name: Visual Testing Customers v2 (Cloudflare) needs: deploy-v2-cloudflare timeout-minutes: 8 - if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bun.lock b/bun.lock index 6e27a46b06..46ac2592dd 100644 --- a/bun.lock +++ b/bun.lock @@ -148,7 +148,7 @@ "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", - "next": "canary", + "next": "^15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", "rison": "^0.1.1", @@ -4077,7 +4077,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook-v2/next": ["next@15.4.0-canary.26", "", { "dependencies": { "@next/env": "15.4.0-canary.26", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.26", "@next/swc-darwin-x64": "15.4.0-canary.26", "@next/swc-linux-arm64-gnu": "15.4.0-canary.26", "@next/swc-linux-arm64-musl": "15.4.0-canary.26", "@next/swc-linux-x64-gnu": "15.4.0-canary.26", "@next/swc-linux-x64-musl": "15.4.0-canary.26", "@next/swc-win32-arm64-msvc": "15.4.0-canary.26", "@next/swc-win32-x64-msvc": "15.4.0-canary.26", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-0lq0x+H4ewc6vXth3S9shrcK3eYl+4wLXQqdboVwBbJe0ykB3+QbGdXFIEICCZsmbAOaii0ag0tzqD3y/vr3bw=="], + "gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="], "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="], @@ -4969,23 +4969,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.26", "", {}, "sha512-+WeMYRfTZWaosbIAjuNESPVjynDz/NKukoR7mF/u3Wuwr40KgScpxD0IuU0T7XbPfprnaInSKAylufFvrXRh+A=="], + "gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HxtmV8Uoai8Z4wAU1tFWzASogAS+xVVP5Z5frbFu0yQ+1ocb9xQTjNqhiD5xPSAU8pNGWasCod8tlTCBzJzHQg=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-1MLiD1Bj6xSi5MkkQ8IK7A13KZJG9bzoWqdXT/tveVCinmYrl/zY7z/9dgvG+84gAE6uN4BGjp6f3IxRsvYDBA=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-cIVgFgOdMDbnPixR/u3ICW60/HlnDbACCb2O+p9+DJj7s1dsN63Cs9qxc9pDJb7tgL0BFPhYcmGeJfd/bZ4h7w=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-o4YS6E3FD2DpZBDvUai9bPMLcpcNZ3THc2BzysSbZeARPiAQuKoudwPJoCpi2t7vajrvczpxBwTPG2uL05ypEA=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-M2/MFrQcPI7Ul5Fq5AOeoARrT0B9SrGiy7BLnPuE7Iai1+xkhfSsxIMF5JeDm/GfJnzcwA2oSvrOg0e7KKdaCA=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.26", "", { "os": "linux", "cpu": "x64" }, "sha512-p5JpQ7k/1LyBzNZglqA8JJm7GRmadPkTyHoWaqMxhiVdcQHGbjwsiNjjAtMNjetNOXxj8ebxjiBsAt+34Ak1IQ=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-FlXIBNOSwnGxxN+HekUfz4Y0n4gPGzqcY3wa3p+5JhzFT7r0oCxMxOdRbs7w8jF5b6uSkWVIQXWFL43F6+8J4g=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.26", "", { "os": "win32", "cpu": "x64" }, "sha512-h9CKrDiEeBof+8IgHStYATYrKVuUt8ggy6429kViWlDbuY6gkuIplf3IRlfpdWAB32I1e4qqUVl/s2xRMgQdqg=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index f19b6951d8..e78e3e2dfe 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -8,7 +8,7 @@ "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", - "next": "canary", + "next": "^15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", "rison": "^0.1.1", From 778624af00e48390d39782b4f63345d1f013708a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 13 May 2025 10:52:58 +0200 Subject: [PATCH 037/127] Only resize images with supported extensions (#3229) --- .changeset/empty-badgers-happen.md | 5 ++ .../lib/images/checkIsSizableImageURL.test.ts | 81 +++++++++++++++++++ .../src/lib/images/checkIsSizableImageURL.ts | 16 +++- .../src/components/DocumentView/Embed.tsx | 10 ++- packages/gitbook/src/lib/paths.test.ts | 16 ++++ packages/gitbook/src/lib/paths.ts | 12 +++ 6 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 .changeset/empty-badgers-happen.md create mode 100644 packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts create mode 100644 packages/gitbook/src/lib/paths.test.ts diff --git a/.changeset/empty-badgers-happen.md b/.changeset/empty-badgers-happen.md new file mode 100644 index 0000000000..75eaad7165 --- /dev/null +++ b/.changeset/empty-badgers-happen.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Only resize images with supported extensions. diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts new file mode 100644 index 0000000000..679f2c5ce7 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'bun:test'; +import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL'; + +describe('checkIsSizableImageURL', () => { + it('should return Skip for non-parsable URLs', () => { + expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for non-http(s) URLs', () => { + expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip); + expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for localhost URLs', () => { + expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe( + SizableImageAction.Skip + ); + expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for GitBook image URLs', () => { + expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe( + SizableImageAction.Skip + ); + }); + + it('should return Resize for supported image extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.png')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe( + SizableImageAction.Resize + ); + }); + + it('should return Resize for URLs without extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize); + }); + + it('should return Passthrough for unsupported image extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe( + SizableImageAction.Passthrough + ); + }); + + it('should handle URLs with query parameters correctly', () => { + expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe( + SizableImageAction.Passthrough + ); + }); + + it('should be case-insensitive for extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe( + SizableImageAction.Resize + ); + }); +}); diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts index 61e3ebbc2b..486ea7a69a 100644 --- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts @@ -1,9 +1,16 @@ +import { getExtension } from '@/lib/paths'; + export enum SizableImageAction { Resize = 'resize', Skip = 'skip', Passthrough = 'passthrough', } +/** + * https://developers.cloudflare.com/images/transform-images/#supported-input-formats + */ +const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + /** * Check if an image URL is resizable. * Skip it for non-http(s) URLs (data, etc). @@ -25,9 +32,12 @@ export function checkIsSizableImageURL(input: string): SizableImageAction { if (parsed.pathname.includes('/~gitbook/image')) { return SizableImageAction.Skip; } - if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { - return SizableImageAction.Passthrough; + + const extension = getExtension(parsed.pathname).toLowerCase(); + if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) { + // If no extension, we consider it resizable. + return SizableImageAction.Resize; } - return SizableImageAction.Resize; + return SizableImageAction.Passthrough; } diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index 796cdaeee3..17849c1958 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -6,6 +6,7 @@ import { Card } from '@/components/primitives'; import { tcls } from '@/lib/tailwind'; import { getDataOrNull } from '@v2/lib/data'; +import { Image } from '../utils'; import type { BlockProps } from './Block'; import { Caption } from './Caption'; import { IntegrationBlock } from './Integration'; @@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) { <Card leadingIcon={ embed.icon ? ( - <img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" /> + <Image + src={embed.icon} + className={tcls('w-5', 'h-5')} + alt="Logo" + sources={{ light: { src: embed.icon } }} + sizes={[{ width: 20 }]} + resize={context.contentContext.imageResizer} + /> ) : null } href={block.data.url} diff --git a/packages/gitbook/src/lib/paths.test.ts b/packages/gitbook/src/lib/paths.test.ts new file mode 100644 index 0000000000..1d418f4988 --- /dev/null +++ b/packages/gitbook/src/lib/paths.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'bun:test'; +import { getExtension } from './paths'; + +describe('getExtension', () => { + it('should return the extension of a path', () => { + expect(getExtension('test.txt')).toBe('.txt'); + }); + + it('should return an empty string if there is no extension', () => { + expect(getExtension('test/path/to/file')).toBe(''); + }); + + it('should return the extension of a path with multiple dots', () => { + expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt'); + }); +}); diff --git a/packages/gitbook/src/lib/paths.ts b/packages/gitbook/src/lib/paths.ts index eff69a569b..ebdf35cd3f 100644 --- a/packages/gitbook/src/lib/paths.ts +++ b/packages/gitbook/src/lib/paths.ts @@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string { return pathname; } + +/** + * Get the extension of a path. + */ +export function getExtension(path: string): string { + const re = /\.[0-9a-z]+$/i; + const match = path.match(re); + if (match) { + return match[0]; + } + return ''; +} From c6637b09e248013f30fccff57a3fa640c9b18243 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Tue, 13 May 2025 22:07:41 +0200 Subject: [PATCH 038/127] Use default value if string/number/boolean in generateSchemaExample (#3234) --- .changeset/ten-apples-march.md | 5 +++++ packages/react-openapi/src/generateSchemaExample.ts | 8 ++++++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/ten-apples-march.md diff --git a/.changeset/ten-apples-march.md b/.changeset/ten-apples-march.md new file mode 100644 index 0000000000..dd13b276c3 --- /dev/null +++ b/.changeset/ten-apples-march.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Use default value if string number or boolean in generateSchemaExample diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index 37695b9fe6..5a038db06b 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -204,6 +204,14 @@ const getExampleFromSchema = ( return cache(schema, schema.example); } + // Use a default value, if there’s one and it’s a string or number + if ( + schema.default !== undefined && + ['string', 'number', 'boolean'].includes(typeof schema.default) + ) { + return cache(schema, schema.default); + } + // enum: [ 'available', 'pending', 'sold' ] if (Array.isArray(schema.enum) && schema.enum.length > 0) { return cache(schema, schema.enum[0]); From 80cb52a2377b10c9be547e31aa6723276d2ac3d3 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 14 May 2025 08:05:08 +0200 Subject: [PATCH 039/127] Handle OpenAPI alternatives from schema.items (#3235) --- .changeset/silly-gorillas-teach.md | 5 +++ packages/react-openapi/src/OpenAPISchema.tsx | 39 +++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 .changeset/silly-gorillas-teach.md diff --git a/.changeset/silly-gorillas-teach.md b/.changeset/silly-gorillas-teach.md new file mode 100644 index 0000000000..b5691ce68a --- /dev/null +++ b/.changeset/silly-gorillas-teach.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Handle OpenAPI alternatives from schema.items diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 7841b1c0f0..e9f8c6707b 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -80,11 +80,10 @@ function OpenAPISchemaProperty( context={context} /> {index < alternatives.length - 1 ? ( - <span className="openapi-schema-alternative-separator"> - {(schema.anyOf || schema.oneOf) && - tString(context.translation, 'or')} - {schema.allOf && tString(context.translation, 'and')} - </span> + <OpenAPISchemaAlternativeSeparator + schema={schema} + context={context} + /> ) : null} </div> ))} @@ -256,6 +255,28 @@ function OpenAPISchemaAlternative(props: { ); } +function OpenAPISchemaAlternativeSeparator(props: { + schema: OpenAPIV3.SchemaObject; + context: OpenAPIClientContext; +}) { + const { schema, context } = props; + + const anyOf = schema.anyOf || schema.items?.anyOf; + const oneOf = schema.oneOf || schema.items?.oneOf; + const allOf = schema.allOf || schema.items?.allOf; + + if (!anyOf && !oneOf && !allOf) { + return null; + } + + return ( + <span className="openapi-schema-alternative-separator"> + {(anyOf || oneOf) && tString(context.translation, 'or')} + {allOf && tString(context.translation, 'and')} + </span> + ); +} + /** * Render a circular reference to a schema. */ @@ -457,6 +478,14 @@ export function getSchemaAlternatives( schema: OpenAPIV3.SchemaObject, ancestors: Set<OpenAPIV3.SchemaObject> = new Set() ): OpenAPIV3.SchemaObject[] | null { + // Search for alternatives in the items property if it exists + if ( + schema.items && + ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items) + ) { + return getSchemaAlternatives(schema.items, ancestors); + } + const alternatives: | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]] | null = (() => { From 08382ce62f9bf59abb7ed093bed99b14b7fd7d8f Mon Sep 17 00:00:00 2001 From: Claire Chabas <claire.chabas@gmail.com> Date: Wed, 14 May 2025 11:10:09 +0200 Subject: [PATCH 040/127] Improve tabs link support (#3211) --- .../DocumentView/HashLinkButton.tsx | 58 +++++++++++++++++ .../src/components/DocumentView/Heading.tsx | 50 +++----------- .../DocumentView/Tabs/DynamicTabs.tsx | 65 ++++++++++++------- .../src/components/DocumentView/Tabs/Tabs.tsx | 5 +- .../src/components/DocumentView/spacing.ts | 5 ++ 5 files changed, 120 insertions(+), 63 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/HashLinkButton.tsx diff --git a/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx new file mode 100644 index 0000000000..02f528c459 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx @@ -0,0 +1,58 @@ +import { type ClassValue, tcls } from '@/lib/tailwind'; +import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api'; +import { Icon } from '@gitbook/icons'; +import { getBlockTextStyle } from './spacing'; + +/** + * A hash icon which adds the block or active block item's ID in the URL hash. + * The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`. + */ +export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash'); + +export function HashLinkButton(props: { + id: string; + block: DocumentBlockTabs | DocumentBlockHeading; + label?: string; + className?: ClassValue; + iconClassName?: ClassValue; +}) { + const { id, block, className, iconClassName, label = 'Direct link to block' } = props; + const textStyle = getBlockTextStyle(block); + return ( + <div + className={tcls( + 'relative', + 'hash', + 'grid', + 'grid-area-1-1', + 'h-[1em]', + 'border-0', + 'opacity-0', + 'group-hover/hash:opacity-[0]', + 'group-focus/hash:opacity-[0]', + 'md:group-hover/hash:md:opacity-[1]', + 'md:group-focus/hash:md:opacity-[1]', + className + )} + > + <a + href={`#${id}`} + aria-label={label} + className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)} + > + <Icon + icon="hashtag" + className={tcls( + 'size-3', + 'self-center', + 'transition-colors', + 'text-transparent', + 'group-hover/hash:text-tint-subtle', + 'contrast-more:group-hover/hash:text-tint-strong', + iconClassName + )} + /> + </a> + </div> + ); +} diff --git a/packages/gitbook/src/components/DocumentView/Heading.tsx b/packages/gitbook/src/components/DocumentView/Heading.tsx index cb11038321..0de49e623f 100644 --- a/packages/gitbook/src/components/DocumentView/Heading.tsx +++ b/packages/gitbook/src/components/DocumentView/Heading.tsx @@ -1,9 +1,9 @@ import type { DocumentBlockHeading } from '@gitbook/api'; -import { Icon } from '@gitbook/icons'; import { tcls } from '@/lib/tailwind'; import type { BlockProps } from './Block'; +import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton'; import { Inlines } from './Inlines'; import { getBlockTextStyle } from './spacing'; @@ -23,50 +23,20 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) { className={tcls( textStyle.textSize, 'heading', - 'group', - 'relative', 'grid', 'scroll-m-12', + hashLinkButtonWrapperStyles, style )} > - <div - className={tcls( - 'hash', - 'grid', - 'grid-area-1-1', - 'relative', - '-ml-6', - 'w-7', - 'border-0', - 'opacity-0', - 'group-hover:opacity-[0]', - 'group-focus:opacity-[0]', - 'md:group-hover:md:opacity-[1]', - 'md:group-focus:md:opacity-[1]', - textStyle.marginTop - )} - > - <a - href={`#${id}`} - aria-label="Direct link to heading" - className={tcls('inline-flex', 'h-full', 'items-start', textStyle.lineHeight)} - > - <Icon - icon="hashtag" - className={tcls( - 'w-3.5', - 'h-[1em]', - 'mt-0.5', - 'transition-colors', - 'text-transparent', - 'group-hover:text-tint-subtle', - 'contrast-more:group-hover:text-tint-strong', - 'lg:w-4' - )} - /> - </a> - </div> + <HashLinkButton + id={id} + block={block} + className={tcls('-ml-6', textStyle.anchorButtonMarginTop)} + iconClassName={tcls('size-4')} + label="Direct link to heading" + /> + <div className={tcls( 'grid-area-1-1', diff --git a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx index 38afbcf086..e4465b3032 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/DynamicTabs.tsx @@ -5,6 +5,8 @@ import React, { useCallback, useMemo } from 'react'; import { useHash, useIsMounted } from '@/components/hooks'; import * as storage from '@/lib/local-storage'; import { type ClassValue, tcls } from '@/lib/tailwind'; +import type { DocumentBlockTabs } from '@gitbook/api'; +import { HashLinkButton, hashLinkButtonWrapperStyles } from '../HashLinkButton'; interface TabsState { activeIds: { @@ -68,9 +70,10 @@ export function DynamicTabs( props: TabsInput & { tabsBody: React.ReactNode[]; style: ClassValue; + block: DocumentBlockTabs; } ) { - const { id, tabs, tabsBody, style } = props; + const { id, block, tabs, tabsBody, style } = props; const hash = useHash(); const [tabsState, setTabsState] = useTabsState(); @@ -146,8 +149,8 @@ export function DynamicTabs( 'ring-inset', 'ring-tint-subtle', 'flex', - 'overflow-hidden', 'flex-col', + 'overflow-hidden', style )} > @@ -165,16 +168,14 @@ export function DynamicTabs( )} > {tabs.map((tab) => ( - <button + <div key={tab.id} - role="tab" - aria-selected={active.id === tab.id} - aria-controls={getTabPanelId(tab.id)} - id={getTabButtonId(tab.id)} - onClick={() => { - onSelectTab(tab); - }} className={tcls( + hashLinkButtonWrapperStyles, + 'flex', + 'items-center', + 'gap-3.5', + //prev from active-tab '[&:has(+_.active-tab)]:rounded-br-md', @@ -184,14 +185,6 @@ export function DynamicTabs( //next from active-tab '[.active-tab_+_:after]:rounded-br-md', - 'inline-block', - 'text-sm', - 'px-3.5', - 'py-2', - 'transition-[color]', - 'font-[500]', - 'relative', - 'after:transition-colors', 'after:border-r', 'after:absolute', @@ -202,14 +195,16 @@ export function DynamicTabs( 'after:h-[70%]', 'after:w-[1px]', + 'px-3.5', + 'py-2', + 'last:after:border-transparent', 'text-tint', 'bg-tint-12/1', 'hover:text-tint-strong', - - 'truncate', 'max-w-full', + 'truncate', active.id === tab.id ? [ @@ -224,8 +219,34 @@ export function DynamicTabs( : null )} > - {tab.title} - </button> + <button + type="button" + role="tab" + aria-selected={active.id === tab.id} + aria-controls={getTabPanelId(tab.id)} + id={getTabButtonId(tab.id)} + onClick={() => { + onSelectTab(tab); + }} + className={tcls( + 'inline-block', + 'text-sm', + 'transition-[color]', + 'font-[500]', + 'relative', + 'max-w-full', + 'truncate' + )} + > + {tab.title} + </button> + + <HashLinkButton + id={getTabButtonId(tab.id)} + block={block} + label="Direct link to tab" + /> + </div> ))} </div> {tabs.map((tab, index) => ( diff --git a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx index 4d5a0d555a..d1ab244852 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx @@ -39,6 +39,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) { <DynamicTabs key={tab.id} id={block.key!} + block={block} tabs={[tab]} tabsBody={[tabsBody[index]]} style={style} @@ -48,5 +49,7 @@ export function Tabs(props: BlockProps<DocumentBlockTabs>) { ); } - return <DynamicTabs id={block.key!} tabs={tabs} tabsBody={tabsBody} style={style} />; + return ( + <DynamicTabs id={block.key!} block={block} tabs={tabs} tabsBody={tabsBody} style={style} /> + ); } diff --git a/packages/gitbook/src/components/DocumentView/spacing.ts b/packages/gitbook/src/components/DocumentView/spacing.ts index a11db3546c..1c21a8b17d 100644 --- a/packages/gitbook/src/components/DocumentView/spacing.ts +++ b/packages/gitbook/src/components/DocumentView/spacing.ts @@ -10,6 +10,8 @@ export function getBlockTextStyle(block: DocumentBlock): { lineHeight: string; /** Tailwind class for the margin top (mt-*) */ marginTop?: string; + /** Tailwind class for the margin top to apply on the anchor link button */ + anchorButtonMarginTop?: string; } { switch (block.type) { case 'paragraph': @@ -22,18 +24,21 @@ export function getBlockTextStyle(block: DocumentBlock): { textSize: 'text-3xl font-semibold', lineHeight: 'leading-tight', marginTop: 'mt-[1em]', + anchorButtonMarginTop: 'mt-[1.05em]', }; case 'heading-2': return { textSize: 'text-2xl font-semibold', lineHeight: 'leading-snug', marginTop: 'mt-[0.75em]', + anchorButtonMarginTop: 'mt-[0.9em]', }; case 'heading-3': return { textSize: 'text-xl font-semibold', lineHeight: 'leading-snug', marginTop: 'mt-[0.5em]', + anchorButtonMarginTop: 'mt-[0.65em]', }; case 'divider': return { From cc37e2ae15ea5896ea28e4401229d458014f00a5 Mon Sep 17 00:00:00 2001 From: spastorelli <spastorelli@users.noreply.github.com> Date: Wed, 14 May 2025 14:16:13 +0200 Subject: [PATCH 041/127] Persist visitor params values in public visitor session cookie (#3231) --- packages/gitbook-v2/src/middleware.ts | 12 +++-- packages/gitbook/src/lib/visitors.test.ts | 42 +++++++++++++---- packages/gitbook/src/lib/visitors.ts | 55 +++++++++++++++++++---- packages/gitbook/src/middleware.ts | 6 +-- 4 files changed, 93 insertions(+), 22 deletions(-) diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 5cb34052f8..916d53d61d 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -10,7 +10,7 @@ import { type ResponseCookies, getPathScopedCookieName, getResponseCookiesForVisitorAuth, - getVisitorPayload, + getVisitorData, normalizeVisitorAuthURL, } from '@/lib/visitors'; import { serveResizedImage } from '@/routes/image'; @@ -95,7 +95,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // // Detect and extract the visitor authentication token from the request // - const { visitorToken, unsignedClaims } = getVisitorPayload({ + const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({ cookies: request.cookies.getAll(), url: siteRequestURL, }); @@ -121,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { apiToken, }) ); - const cookies: ResponseCookies = []; + + const cookies: ResponseCookies = visitorParamsCookie + ? [ + // If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation. + visitorParamsCookie, + ] + : []; // // Handle redirects diff --git a/packages/gitbook/src/lib/visitors.test.ts b/packages/gitbook/src/lib/visitors.test.ts index 82060fcdfa..4337371fa6 100644 --- a/packages/gitbook/src/lib/visitors.test.ts +++ b/packages/gitbook/src/lib/visitors.test.ts @@ -178,7 +178,7 @@ describe('getVisitorUnsignedClaims', () => { const url = new URL('https://example.com/'); const claims = getVisitorUnsignedClaims({ cookies, url }); - expect(claims).toStrictEqual({ + expect(claims.all).toStrictEqual({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } }, launchdarkly: { flags: { ALPHA: true, API: true } }, }); @@ -191,7 +191,12 @@ describe('getVisitorUnsignedClaims', () => { const claims = getVisitorUnsignedClaims({ cookies: [], url }); - expect(claims).toStrictEqual({ + expect(claims.all).toStrictEqual({ + isEnterprise: true, + language: 'fr', + country: 'fr', + }); + expect(claims.fromVisitorParams).toStrictEqual({ isEnterprise: true, language: 'fr', country: 'fr', @@ -203,10 +208,13 @@ describe('getVisitorUnsignedClaims', () => { const claims = getVisitorUnsignedClaims({ cookies: [], url }); - expect(claims).toStrictEqual({ + expect(claims.all).toStrictEqual({ isEnterprise: true, // otherParam is not present }); + expect(claims.fromVisitorParams).toStrictEqual({ + isEnterprise: true, + }); }); it('should support nested query param keys via dot notation', () => { @@ -216,7 +224,14 @@ describe('getVisitorUnsignedClaims', () => { const claims = getVisitorUnsignedClaims({ cookies: [], url }); - expect(claims).toStrictEqual({ + expect(claims.all).toStrictEqual({ + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + expect(claims.fromVisitorParams).toStrictEqual({ isEnterprise: true, flags: { ALPHA: true, @@ -235,7 +250,7 @@ describe('getVisitorUnsignedClaims', () => { const url = new URL('https://example.com/'); const claims = getVisitorUnsignedClaims({ cookies, url }); - expect(claims).toStrictEqual({}); + expect(claims.all).toStrictEqual({}); }); it('should merge claims from cookies and visitor.* query params', () => { @@ -250,16 +265,27 @@ describe('getVisitorUnsignedClaims', () => { }, ]; const url = new URL( - 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false' + 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false' ); const claims = getVisitorUnsignedClaims({ cookies, url }); - expect(claims).toStrictEqual({ + expect(claims.all).toStrictEqual({ role: 'admin', language: 'fr', bucket: { - flags: { SITE_AI: true, SITE_PREVIEW: true }, + flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true }, + }, + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + + expect(claims.fromVisitorParams).toStrictEqual({ + bucket: { + flags: { HELLO: false }, }, isEnterprise: true, flags: { diff --git a/packages/gitbook/src/lib/visitors.ts b/packages/gitbook/src/lib/visitors.ts index c6a7aae986..7ae970666b 100644 --- a/packages/gitbook/src/lib/visitors.ts +++ b/packages/gitbook/src/lib/visitors.ts @@ -41,13 +41,15 @@ type ClaimPrimitive = | ClaimPrimitive[]; /** - * The result of a visitor info lookup that can include: + * The result of a visitor data lookup that can include: * - a visitor token (JWT) * - a record of visitor public/unsigned claims (JSON object) + * - a session cookie response to persist any visitor query params across navigations. */ -export type VisitorPayloadLookup = { +export type VisitorDataLookup = { visitorToken: VisitorTokenLookup; unsignedClaims: Record<string, ClaimPrimitive>; + visitorParamsCookie: ResponseCookie | undefined; }; /** @@ -74,21 +76,26 @@ export type VisitorTokenLookup = | undefined; /** - * Get the visitor info for the request including its token and/or unsigned claims when present. + * Get the visitor data for the request potentially including: + * - a JWT token that may contain signed claims or can be used for VA authentication. + * - a record of the unsigned claims passed via a cookie or visitor.* params. + * - a session cookie response that is used to persist any visitor.* params that were passed via the site URL. */ -export function getVisitorPayload({ +export function getVisitorData({ cookies, url, }: { cookies: RequestCookies; url: URL | NextRequest['nextUrl']; -}): VisitorPayloadLookup { +}): VisitorDataLookup { const visitorToken = getVisitorToken({ cookies, url }); const unsignedClaims = getVisitorUnsignedClaims({ cookies, url }); + const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams); return { visitorToken, - unsignedClaims, + unsignedClaims: unsignedClaims.all, + visitorParamsCookie, }; } @@ -128,9 +135,19 @@ export function getVisitorToken({ export function getVisitorUnsignedClaims(args: { cookies: RequestCookies; url: URL | NextRequest['nextUrl']; -}): Record<string, ClaimPrimitive> { +}): { + /** + * The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params. + */ + all: Record<string, ClaimPrimitive>; + /** + * The unsigned claims from the `visitor.*` query params. + */ + fromVisitorParams: Record<string, ClaimPrimitive>; +} { const { cookies, url } = args; const claims: Record<string, ClaimPrimitive> = {}; + const searchParamsClaims: Record<string, ClaimPrimitive> = {}; for (const cookie of cookies) { if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) { @@ -149,11 +166,13 @@ export function getVisitorUnsignedClaims(args: { if (key.startsWith('visitor.')) { const claimPath = key.substring('visitor.'.length); const claimValue = parseVisitorQueryParamValue(value); + setVisitorClaimByPath(claims, claimPath, claimValue); + setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue); } } - return claims; + return { all: claims, fromVisitorParams: searchParamsClaims }; } /** @@ -221,6 +240,26 @@ function parseVisitorQueryParamValue(value: string): ClaimPrimitive { return value; } +/** + * Returns to cookie response to use in order to persist visitor params that were passed to the URL. + */ +function getResponseCookieForVisitorParams( + visitorParamsClaims: Record<string, ClaimPrimitive> +): ResponseCookie | undefined { + if (Object.keys(visitorParamsClaims).length === 0) { + return undefined; + } + + return { + name: VISITOR_UNSIGNED_CLAIMS_PREFIX, + value: JSON.stringify(visitorParamsClaims), + options: { + sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined, + secure: process.env.NODE_ENV === 'production', + }, + }; +} + /** * Return the lookup result for content served with visitor auth. */ diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 04b1b40dd9..3e5f682ccc 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -24,7 +24,7 @@ import { type ResponseCookies, type VisitorTokenLookup, getResponseCookiesForVisitorAuth, - getVisitorPayload, + getVisitorData, normalizeVisitorAuthURL, } from '@/lib/visitors'; @@ -392,7 +392,7 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup * When serving multi spaces based on the current URL. */ async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> { - const { visitorToken } = getVisitorPayload({ + const { visitorToken } = getVisitorData({ cookies: request.cookies.getAll(), url, }); @@ -609,7 +609,7 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis const target = new URL(targetStr); target.search = url.search; - const { visitorToken } = getVisitorPayload({ + const { visitorToken } = getVisitorData({ cookies: request.cookies.getAll(), url: target, }); From 95a1f652f9df864f52467b84fb5a85e62936e2fb Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Thu, 15 May 2025 13:24:10 +0200 Subject: [PATCH 042/127] Better print layouts: wrap code blocks & force table column auto-sizing (#3236) --- .changeset/rare-radios-thank.md | 5 +++++ .../components/DocumentView/CodeBlock/CodeBlockRenderer.tsx | 2 +- .../gitbook/src/components/DocumentView/Table/RecordRow.tsx | 4 ++-- .../gitbook/src/components/DocumentView/Table/ViewGrid.tsx | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/rare-radios-thank.md diff --git a/.changeset/rare-radios-thank.md b/.changeset/rare-radios-thank.md new file mode 100644 index 0000000000..0fb143ce42 --- /dev/null +++ b/.changeset/rare-radios-thank.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Better print layouts: wrap code blocks & force table column auto-sizing diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx index d8f35d90bb..0ad885a444 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx @@ -58,7 +58,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( <code id={id} className={tcls( - 'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line]', + 'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap', withWrap && 'whitespace-pre-wrap' )} > diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx index e39f386cc5..b09760ceb2 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx @@ -15,14 +15,14 @@ export function RecordRow( fixedColumns: string[]; } ) { - const { view, autoSizedColumns, fixedColumns, block } = props; + const { view, autoSizedColumns, fixedColumns, block, context } = props; return ( <div className={styles.row} role="row"> {view.columns.map((column) => { const columnWidth = getColumnWidth({ column, - columnWidths: view.columnWidths, + columnWidths: context.mode === 'print' ? undefined : view.columnWidths, autoSizedColumns, fixedColumns, }); diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx index 50aa13565b..9f2b98ba44 100644 --- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx @@ -13,10 +13,10 @@ import { getColumnAlignment } from './utils'; 3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px */ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) { - const { block, view, records, style } = props; + const { block, view, records, style, context } = props; /* Calculate how many columns are auto-sized vs fixed width */ - const columnWidths = view.columnWidths; + const columnWidths = context.mode === 'print' ? undefined : view.columnWidths; const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]); const fixedColumns = view.columns.filter((column) => columnWidths?.[column]); From 3460b78b45942e2edb44c82ad90207ac8173e57d Mon Sep 17 00:00:00 2001 From: Addison <42930383+addisonschultz@users.noreply.github.com> Date: Fri, 16 May 2025 07:29:53 -0500 Subject: [PATCH 043/127] Update docs.gitbook.com to gitbook.com/docs (#3187) Co-authored-by: Tal Gluck <talagluck@gmail.com> Co-authored-by: Taran Vohra <taranvohra@outlook.com> --- .github/CONTRIBUTING.md | 4 ++-- .github/workflows/deploy-preview.yaml | 2 +- README.md | 4 ++-- packages/gitbook/CHANGELOG.md | 2 +- packages/gitbook/e2e/internal.spec.ts | 2 +- packages/gitbook/e2e/util.ts | 1 + .../src/components/SiteLayout/RocketLoaderDetector.tsx | 2 +- packages/gitbook/tests/pagespeed-testing.ts | 4 ++-- 8 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a65357a23d..b93b518961 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`. When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`. -For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server. +For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/gitbook.com/docs` after running the development server. -You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version. +You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version. ### Commit your update diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index 4b14e3f83d..7d8eec6109 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -152,7 +152,7 @@ jobs: | Site | `v1` | `2v` | `2c` | | --- | --- | --- | --- | - | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) | + | GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) | | E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | edit-mode: replace visual-testing-v1: diff --git a/README.md b/README.md index 5a055a7a75..f9702f88e3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ <h1 align="center">GitBook</h1> <p align="center"> - <a href="https://docs.gitbook.com/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> + <a href="https://gitbook.com/docs/">Docs</a> - <a href="https://github.com/GitbookIO/community">Community</a> - <a href="https://developer.gitbook.com/">Developer Docs</a> - <a href="https://changelog.gitbook.com/">Changelog</a> - <a href="https://github.com/GitbookIO/gitbook/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a> </p> <p align="center"> @@ -66,7 +66,7 @@ bun dev:v2 examples: -- http://localhost:3000/url/docs.gitbook.com +- http://localhost:3000/url/gitbook.com/docs - http://localhost:3000/url/open-source.gitbook.io/midjourney Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser. diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md index dc285100ff..21003be437 100644 --- a/packages/gitbook/CHANGELOG.md +++ b/packages/gitbook/CHANGELOG.md @@ -564,7 +564,7 @@ - 4cbcc5b: Rollback of scalar modal while fixing perf issue - 3996110: Optimize images rendered in community ads - 133c3e7: Update design of Checkbox to be more consistent and readable -- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency +- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency - 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations - 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level - b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected` diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 83181c3c9a..a70a7e5d8d 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -269,7 +269,7 @@ const testCases: TestsCase[] = [ }, { name: 'GitBook', - contentBaseURL: 'https://docs.gitbook.com', + contentBaseURL: 'https://gitbook.com/docs/', tests: [ { name: 'Home', diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 78d7badb71..61113f8264 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -168,6 +168,7 @@ export function runTestCases(testCases: TestsCase[]) { new URL(testEntryPathname, testCase.contentBaseURL).toString() ) : getTestURL(testEntryPathname); + if (testEntry.cookies) { await context.addCookies( testEntry.cookies.map((cookie) => ({ diff --git a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx index 096024089e..cc689bbef1 100644 --- a/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx +++ b/packages/gitbook/src/components/SiteLayout/RocketLoaderDetector.tsx @@ -18,7 +18,7 @@ export function RocketLoaderDetector(props: { nonce?: string }) { alert.className = 'p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 mt-8 mx-8'; alert.innerHTML = \` <strong>Error in site configuration:</strong> - It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://docs.gitbook.com/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem. + It looks like \${window.location.hostname} has been incorrectly configured in Cloudflare. This may lead to unexpected behavior or issues with the page loading. If you are the owner of this site, please refer to <a href="https://gitbook.com/docs/published-documentation/custom-domain/configure-dns#are-you-using-cloudflare" class="underline">GitBook's documentation</a> for steps to fix the problem. \`; document.body.prepend(alert); diff --git a/packages/gitbook/tests/pagespeed-testing.ts b/packages/gitbook/tests/pagespeed-testing.ts index 94f48d4201..07f1f01b3d 100644 --- a/packages/gitbook/tests/pagespeed-testing.ts +++ b/packages/gitbook/tests/pagespeed-testing.ts @@ -12,13 +12,13 @@ interface Test { // and to be able to see the results, and only catch major regressions. const tests: Array<Test> = [ { - url: 'https://docs.gitbook.com', + url: 'https://gitbook.com/docs', strategy: 'desktop', threshold: 60, }, { - url: 'https://docs.gitbook.com', + url: 'https://gitbook.com/docs', strategy: 'mobile', threshold: 30, }, From 2c85a302fc7e99d3fb8a7aad9ea215f3e1c7a79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 16 May 2025 13:58:35 +0100 Subject: [PATCH 044/127] Update opennext and leverage "use cache" on CF (#3204) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- bun.lock | 6 +- package.json | 2 +- packages/gitbook-v2/open-next.config.ts | 25 +- packages/gitbook-v2/package.json | 2 +- packages/gitbook-v2/src/lib/data/api.ts | 1381 ++++++--------------- packages/gitbook-v2/wrangler.jsonc | 6 +- packages/gitbook/src/lib/openapi/fetch.ts | 16 +- packages/gitbook/src/lib/tracing.ts | 6 +- 8 files changed, 437 insertions(+), 1007 deletions(-) diff --git a/bun.lock b/bun.lock index 46ac2592dd..a7d865c06d 100644 --- a/bun.lock +++ b/bun.lock @@ -145,6 +145,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -156,7 +157,6 @@ "warn-once": "^0.1.1", }, "devDependencies": { - "@opennextjs/cloudflare": "^1.0.0-beta.3", "@types/rison": "^0.0.9", "gitbook": "*", "postcss": "^8", @@ -791,9 +791,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/@opennextjs/cloudflare@666", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], diff --git a/package.json b/package.json index 447997ab82..5398e52f9f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "turbo": "^2.5.0", "vercel": "^39.3.0" }, - "packageManager": "bun@1.2.8", + "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", "@gitbook/api": "^0.115.0", diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 8f4d389da6..4ff4f74e88 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -3,16 +3,27 @@ import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cac import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; +import { + softTagFilter, + withFilter, +} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; export default defineCloudflareConfig({ incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }), - tagCache: doShardedTagCache({ - baseShardSize: 12, - regionalCache: true, - shardReplication: { - numberOfSoftReplicas: 2, - numberOfHardReplicas: 1, - }, + tagCache: withFilter({ + tagCache: doShardedTagCache({ + baseShardSize: 12, + regionalCache: true, + shardReplication: { + numberOfSoftReplicas: 2, + numberOfHardReplicas: 1, + }, + }), + // We don't use `revalidatePath`, so we filter out soft tags + filterFn: softTagFilter, }), queue: doQueue, + + // Performance improvements as we don't use PPR + enableCacheInterception: true, }); diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index e78e3e2dfe..646bfb4719 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,6 +5,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -17,7 +18,6 @@ }, "devDependencies": { "gitbook": "*", - "@opennextjs/cloudflare": "^1.0.0-beta.3", "@types/rison": "^0.0.9", "tailwindcss": "^3.4.0", "postcss": "^8" diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 20e495656e..7a2d03d16d 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -7,14 +7,8 @@ import { type RenderIntegrationUI, } from '@gitbook/api'; import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; -import { - GITBOOK_API_TOKEN, - GITBOOK_API_URL, - GITBOOK_RUNTIME, - GITBOOK_USER_AGENT, -} from '@v2/lib/env'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; -import { unstable_cache } from 'next/cache'; import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare'; import { DataFetcherError, wrapDataFetcherError } from './errors'; import { withCacheKey, withoutConcurrentExecution } from './memoize'; @@ -28,16 +22,13 @@ interface DataFetcherInput { } /** - * Revalidation profile for the cache. - * Based on https://nextjs.org/docs/app/api-reference/functions/cacheLife#default-cache-profiles + * Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`. */ -enum RevalidationProfile { - minutes = 60, - hours = 60 * 60, - days = 60 * 60 * 24, - weeks = 60 * 60 * 24 * 7, - max = 60 * 60 * 24 * 30, -} +export const noCacheFetchOptions: Partial<RequestInit> = { + next: { + revalidate: 0, + }, +}; /** * Create a data fetcher using an API token. @@ -206,589 +197,301 @@ export function createDataFetcher( }; } -/* - * For the following functions, we: - * - Wrap them with `withCacheKey` to compute a cache key from the function arguments ONCE (to be performant) - * - Pass the cache key to `unstable_cache` to ensure the cache is not tied to closures - * - Call the uncached function in a `withoutConcurrentExecution` wrapper to prevent concurrent executions - * - * Important: - * - Only the function inside the `unstable_cache` is wrapped in `withoutConcurrentExecution` as Next.js needs to call - * the return of `unstable_cache` to identify the tags. - */ - const getUserById = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async (cacheKey, input: DataFetcherInput, params: { userId: string }) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getUserByIdUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getUserByIdUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [], - } - ); - - return uncached(); + async (_, input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, + }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const getUserByIdUseCache = async (input: DataFetcherInput, params: { userId: string }) => { - 'use cache'; - return getUserByIdUncached(input, params, true); -}; - -const getUserByIdUncached = async ( - input: DataFetcherInput, - params: { userId: string }, - withUseCache = false -) => { - return trace(`getUserById.uncached(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.users.getUserById(params.userId); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - const getSpace = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getSpaceUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getSpaceUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'space', - space: params.spaceId, - }), - ], - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) ); - return uncached(); + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const getSpaceUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined } -) => { - 'use cache'; - return getSpaceUncached(input, params, true); -}; - -const getSpaceUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - } - - return trace(`getSpace.uncached(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getSpaceById(params.spaceId, { - shareKey: params.shareKey, - }); - - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - const getChangeRequest = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; changeRequestId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getChangeRequestUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { - return getChangeRequestUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.minutes * 5, - tags: [ - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getChangeRequestUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string } -) => { - 'use cache'; - return getChangeRequestUncached(input, params, true); -}; - -const getChangeRequestUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }) - ); - } - - return trace( - `getChangeRequest.uncached(${params.spaceId}, ${params.changeRequestId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - } - return res.data; - }); - } - ); -}; - const getRevision = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; metadata: boolean } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getRevisionUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); } ) ); -const getRevisionUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return getRevisionUncached(input, params, true); -}; - -const getRevisionUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean }, - withUseCache = false -) => { - return trace(`getRevision.uncached(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - }); -}; - const getRevisionPages = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; metadata: boolean } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPagesUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getRevisionPagesUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); } ) ); -const getRevisionPagesUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return getRevisionPagesUncached(input, params, true); -}; - -const getRevisionPagesUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean }, - withUseCache = false -) => { - return trace(`getRevisionPages.uncached(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data.pages; - }); - }); -}; - const getRevisionFile = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; fileId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionFileUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, async () => { - return getRevisionFileUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getRevisionFileUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string } -) => { - 'use cache'; - return getRevisionFileUncached(input, params, true); -}; - -const getRevisionFileUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string }, - withUseCache = false -) => { - return trace( - `getRevisionFile.uncached(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {} - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getRevisionPageMarkdown = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; pageId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPageMarkdownUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, async () => { - return getRevisionPageMarkdownUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + format: 'markdown', + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); + } + return res.data.markdown; + }); } ); - - return uncached(); } ) ); -const getRevisionPageMarkdownUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } -) => { - 'use cache'; - return getRevisionPageMarkdownUncached(input, params, true); -}; - -const getRevisionPageMarkdownUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string }, - withUseCache = false -) => { - return trace( - `getRevisionPageMarkdown.uncached(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - format: 'markdown', - } - ); - - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); - } - return res.data.markdown; - }); - } - ); -}; - const getRevisionPageByPath = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; path: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getRevisionPageByPathUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, async () => { - return getRevisionPageByPathUncached(input, params); - }, - [cacheKey, 'v2'], - { - revalidate: RevalidationProfile.max, - tags: [], + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getRevisionPageByPathUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string } -) => { - 'use cache'; - return getRevisionPageByPathUncached(input, params, true); -}; - -const getRevisionPageByPathUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string }, - withUseCache = false -) => { - return trace( - `getRevisionPageByPath.uncached(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {} - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getDocument = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async ( - cacheKey, - input: DataFetcherInput, - params: { spaceId: string; documentId: string } - ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getDocumentUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getDocumentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], - } - ); - - return uncached(); + async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); } ) ); -const getDocumentUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; documentId: string } -) => { - 'use cache'; - return getDocumentUncached(input, params, true); -}; - -const getDocumentUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; documentId: string }, - withUseCache = false -) => { - return trace(`getDocument.uncached(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {}); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - }); -}; - const getComputedDocument = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; @@ -797,309 +500,157 @@ const getComputedDocument = withCacheKey( seed: string; } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getComputedDocumentUseCache(input, params); - } + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( + { + spaceId: params.spaceId, + organizationId: params.organizationId, + }, + params.source + ) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, async () => { - return getComputedDocumentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ), + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getComputedDocument( + params.spaceId, + { + source: params.source, + seed: params.seed, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getComputedDocumentUseCache = async ( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - } -) => { - 'use cache'; - return getComputedDocumentUncached(input, params, true); -}; - -const getComputedDocumentUncached = async ( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - ...getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ) - ); - } - - return trace( - `getComputedDocument.uncached(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getComputedDocument(params.spaceId, { - source: params.source, - seed: params.seed, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getReusableContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { spaceId: string; revisionId: string; reusableContentId: string } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getReusableContentUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, async () => { - return getReusableContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getReusableContentUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string } -) => { - 'use cache'; - return getReusableContentUncached(input, params, true); -}; - -const getReusableContentUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string }, - withUseCache = false -) => { - return trace( - `getReusableContent.uncached(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getLatestOpenAPISpecVersionContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async ( - cacheKey, - input: DataFetcherInput, - params: { organizationId: string; slug: string } - ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getLatestOpenAPISpecVersionContentUseCache(input, params); - } + async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'openapi', + organization: params.organizationId, + openAPISpec: params.slug, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, async () => { - return getLatestOpenAPISpecVersionContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.max, - tags: [ - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getLatestOpenAPISpecVersionContentUseCache = async ( - input: DataFetcherInput, - params: { organizationId: string; slug: string } -) => { - 'use cache'; - return getLatestOpenAPISpecVersionContentUncached(input, params, true); -}; - -const getLatestOpenAPISpecVersionContentUncached = async ( - input: DataFetcherInput, - params: { organizationId: string; slug: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }) - ); - } - - return trace( - `getLatestOpenAPISpecVersionContent.uncached(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - } - return res.data; - }); - } - ); -}; - const getPublishedContentSite = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { organizationId: string; siteId: string; siteShareKey: string | undefined } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getPublishedContentSiteUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, async () => { - return getPublishedContentSiteUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'site', - site: params.siteId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getPublishedContentSiteUseCache = async ( - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined } -) => { - 'use cache'; - return getPublishedContentSiteUncached(input, params, true); -}; - -const getPublishedContentSiteUncached = async ( - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `getPublishedContentSite.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - } - ); -}; - const getSiteRedirectBySource = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { organizationId: string; @@ -1108,293 +659,169 @@ const getSiteRedirectBySource = withCacheKey( source: string; } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getSiteRedirectBySourceUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, async () => { - return getSiteRedirectBySourceUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'site', - site: params.siteId, - }), - ], + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } ); - - return uncached(); } ) ); -const getSiteRedirectBySourceUseCache = async ( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - } -) => { - 'use cache'; - return getSiteRedirectBySourceUncached(input, params, true); -}; - -const getSiteRedirectBySourceUncached = async ( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `getSiteRedirectBySource.uncached(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - } - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - } - ); -}; - const getEmbedByUrl = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, - async (cacheKey, input: DataFetcherInput, params: { spaceId: string; url: string }) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return getEmbedByUrlUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return getEmbedByUrlUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.weeks, - tags: [], - } + async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) ); - return uncached(); + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); + }); } ) ); -const getEmbedByUrlUseCache = async ( - input: DataFetcherInput, - params: { spaceId: string; url: string } -) => { - 'use cache'; - return getEmbedByUrlUncached(input, params, true); -}; - -const getEmbedByUrlUncached = async ( - input: DataFetcherInput, - params: { spaceId: string; url: string }, - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - } - - return trace(`getEmbedByUrl.uncached(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { - url: params.url, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('weeks'); - } - return res.data; - }); - }); -}; - const searchSiteContent = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return searchSiteContentUseCache(input, params); - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, async () => { - return searchSiteContentUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.hours, - tags: [], + return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; + const api = apiClient(input); + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('hours'); + return res.data.items; + }); } ); - - return uncached(); } ) ); -const searchSiteContentUseCache = async ( - input: DataFetcherInput, - params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] -) => { - 'use cache'; - return searchSiteContentUncached(input, params, true); -}; - -const searchSiteContentUncached = async ( - input: DataFetcherInput, - params: Parameters<GitBookDataFetcher['searchSiteContent']>[0], - withUseCache = false -) => { - if (withUseCache) { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - } - - return trace( - `searchSiteContent.uncached(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, scope } = params; - const api = apiClient(input); - const res = await api.orgs.searchSiteContent(organizationId, siteId, { - query, - ...scope, - }); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('hours'); - } - return res.data.items; - }); - } - ); -}; - const renderIntegrationUi = withCacheKey( withoutConcurrentExecution( getCloudflareRequestGlobal, async ( - cacheKey, + _, input: DataFetcherInput, params: { integrationName: string; request: RenderIntegrationUI } ) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return renderIntegrationUiUseCache(input, params); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache( - async () => { - return renderIntegrationUiUncached(input, params); - }, - [cacheKey], - { - revalidate: RevalidationProfile.days, - tags: [ - getCacheTag({ - tag: 'integration', - integration: params.integrationName, - }), - ], - } + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) ); - return uncached(); + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); } ) ); -const renderIntegrationUiUseCache = async ( - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI } -) => { - 'use cache'; - return renderIntegrationUiUncached(input, params, true); -}; - -const renderIntegrationUiUncached = ( - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI }, - withUseCache = false -) => { - return trace(`renderIntegrationUi.uncached(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request - ); - if (withUseCache) { - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - } - return res.data; - }); - }); -}; - async function* streamAIResponse( input: DataFetcherInput, params: Parameters<GitBookDataFetcher['streamAIResponse']>[0] ) { const api = apiClient(input); - const res = await api.orgs.streamAiResponseInSite(params.organizationId, params.siteId, { - input: params.input, - output: params.output, - model: params.model, - }); + const res = await api.orgs.streamAiResponseInSite( + params.organizationId, + params.siteId, + { + input: params.input, + output: params.output, + model: params.model, + }, + { + ...noCacheFetchOptions, + } + ); for await (const event of res) { yield event; diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index e316e2a242..7fbecf3bc6 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -2,7 +2,11 @@ "main": ".open-next/worker.js", "name": "gitbook-open-v2", "compatibility_date": "2025-04-14", - "compatibility_flags": ["nodejs_compat", "allow_importable_env"], + "compatibility_flags": [ + "nodejs_compat", + "allow_importable_env", + "global_fetch_strictly_public" + ], "assets": { "directory": ".open-next/assets", "binding": "ASSETS" diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index d1f8c03cef..b496bbe59f 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -1,5 +1,4 @@ import { parseOpenAPI } from '@gitbook/openapi-parser'; -import { unstable_cache } from 'next/cache'; import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache'; import type { @@ -10,7 +9,6 @@ import type { } from '@/lib/openapi/types'; import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare'; import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize'; -import { GITBOOK_RUNTIME } from '@v2/lib/env'; import { assert } from 'ts-essentials'; import { resolveContentRef } from '../references'; import { isV2 } from '../v2'; @@ -71,18 +69,8 @@ const fetchFilesystemV1 = cache({ }); const fetchFilesystemV2 = withCacheKey( - withoutConcurrentExecution(getCloudflareRequestGlobal, async (cacheKey, url: string) => { - if (GITBOOK_RUNTIME !== 'cloudflare') { - return fetchFilesystemUseCache(url); - } - - // FIXME: OpenNext doesn't support 'use cache' yet - const uncached = unstable_cache(async () => fetchFilesystemUncached(url), [cacheKey], { - revalidate: 60 * 60 * 24, - }); - - const response = await uncached(); - return response; + withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => { + return fetchFilesystemUseCache(url); }) ); diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts index 38db339240..71b5c072e8 100644 --- a/packages/gitbook/src/lib/tracing.ts +++ b/packages/gitbook/src/lib/tracing.ts @@ -28,19 +28,19 @@ export async function trace<T>( }; const start = now(); - let failed = false; + let traceError: null | Error = null; try { return await fn(span); } catch (error) { span.setAttribute('error', true); - failed = true; + traceError = error as Error; throw error; } finally { if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') { const end = now(); // biome-ignore lint/suspicious/noConsole: we want to log performance data console.log( - `trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`, + `trace ${completeName} ${traceError ? `failed with ${traceError.message}` : 'succeeded'} in ${end - start}ms`, attributes ); } From d00dc8ca72e796cc8698df5a04463fc94fe90433 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Sun, 18 May 2025 13:20:17 +0200 Subject: [PATCH 045/127] Pass spec validation errors through OpenAPIParseError (#3240) --- .changeset/four-jobs-reply.md | 5 +++++ packages/openapi-parser/src/error.ts | 6 +++++- packages/openapi-parser/src/v3.ts | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/four-jobs-reply.md diff --git a/.changeset/four-jobs-reply.md b/.changeset/four-jobs-reply.md new file mode 100644 index 0000000000..714b8deef3 --- /dev/null +++ b/.changeset/four-jobs-reply.md @@ -0,0 +1,5 @@ +--- +'@gitbook/openapi-parser': patch +--- + +Pass scalar's errors through OpenAPIParseError diff --git a/packages/openapi-parser/src/error.ts b/packages/openapi-parser/src/error.ts index e0d25d3e29..a7c5398bcb 100644 --- a/packages/openapi-parser/src/error.ts +++ b/packages/openapi-parser/src/error.ts @@ -1,3 +1,5 @@ +import type { ErrorObject } from '@scalar/openapi-parser'; + type OpenAPIParseErrorCode = | 'invalid' | 'parse-v2-in-v3' @@ -12,17 +14,19 @@ export class OpenAPIParseError extends Error { public override name = 'OpenAPIParseError'; public code: OpenAPIParseErrorCode; public rootURL: string | null; - + public errors: ErrorObject[] | undefined; constructor( message: string, options: { code: OpenAPIParseErrorCode; rootURL?: string | null; cause?: Error; + errors?: ErrorObject[] | undefined; } ) { super(message, { cause: options.cause }); this.code = options.code; this.rootURL = options.rootURL ?? null; + this.errors = options.errors; } } diff --git a/packages/openapi-parser/src/v3.ts b/packages/openapi-parser/src/v3.ts index 14a890769a..3b54819cff 100644 --- a/packages/openapi-parser/src/v3.ts +++ b/packages/openapi-parser/src/v3.ts @@ -40,6 +40,7 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) { throw new OpenAPIParseError('Invalid OpenAPI document', { code: 'invalid', rootURL, + errors: result.errors, }); } From 12a455dab5b35e4a68e51ca9e002065f3c52e364 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Sun, 18 May 2025 14:50:05 +0200 Subject: [PATCH 046/127] Fix OpenAPI layout issues (#3242) --- .changeset/strong-eggs-glow.md | 5 +++++ .../gitbook/src/components/DocumentView/OpenAPI/style.css | 8 ++++---- packages/gitbook/src/components/PageAside/PageAside.tsx | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/strong-eggs-glow.md diff --git a/.changeset/strong-eggs-glow.md b/.changeset/strong-eggs-glow.md new file mode 100644 index 0000000000..7c1e1db039 --- /dev/null +++ b/.changeset/strong-eggs-glow.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix OpenAPI layout issues diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index f3c24cb738..213d12dec5 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -145,7 +145,7 @@ } .openapi-column-preview-body { - @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static; + @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static; } .openapi-column-preview pre { @@ -770,7 +770,7 @@ body:has(.openapi-select-popover) { } .openapi-disclosure:has(> .openapi-disclosure-trigger:hover) { - @apply bg-tint-subtle; + @apply bg-tint-subtle overflow-hidden; } .openapi-disclosure:has(> .openapi-disclosure-trigger:hover), @@ -778,10 +778,10 @@ body:has(.openapi-select-popover) { @apply ring-1 shadow-sm; } -.openapi-disclosure[data-expanded="true"]:not(:first-child) { +.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) { @apply mt-2; } -.openapi-disclosure[data-expanded="true"]:not(:last-child) { +.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) { @apply mb-2; } diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index a51410cbc0..06821d964c 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -87,7 +87,7 @@ export function PageAside(props: { 'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1', 'page-api-block:xl:max-2xl:rounded-md', 'page-api-block:xl:max-2xl:h-auto', - 'page-api-block:xl:max-2xl:my-8', + 'page-api-block:xl:max-2xl:my-4', 'page-api-block:p-2' )} > From 223edae17daee2a7a0cecf39b5e0e93cefb12a1b Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 19 May 2025 13:47:33 +0200 Subject: [PATCH 047/127] Add env variables for the DOQueue (#3243) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- bun.lock | 4 ++-- packages/gitbook-v2/package.json | 2 +- packages/gitbook-v2/wrangler.jsonc | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index a7d865c06d..2c13245d05 100644 --- a/bun.lock +++ b/bun.lock @@ -145,7 +145,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", + "@opennextjs/cloudflare": "1.0.3", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -793,7 +793,7 @@ "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/@opennextjs/cloudflare@666", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-+SNhz2JmKkMlWzndTSYZeq0eseAPfEXUUYJA7H9MkgFe36i1no0OdiLuwu9e4uR3yuOzaNWB+skCYHsD6Im9nA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 646bfb4719..1db128e211 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,7 +5,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "https://pkg.pr.new/@opennextjs/cloudflare@666", + "@opennextjs/cloudflare": "1.0.3", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index 7fbecf3bc6..6d8ccf9955 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -14,6 +14,9 @@ "observability": { "enabled": true }, + "vars": { + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + }, "env": { "preview": { "r2_buckets": [ @@ -87,6 +90,11 @@ ] }, "production": { + "vars": { + // This is a bit misleading, but it means that we can have 100 concurrent revalidations + // This means that we'll have up to 20 durable objects instance running at the same time + "MAX_REVALIDATE_CONCURRENCY": "20" + }, "routes": [ { "pattern": "open-2c.gitbook.com/*", From 1c8d9febb3c721f8dd8ae33a0a5142ff0b47c967 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 19 May 2025 17:00:09 +0200 Subject: [PATCH 048/127] Custom Incremental Cache (#3239) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/clean-crabs-tie.md | 5 + packages/gitbook-v2/.gitignore | 1 + packages/gitbook-v2/open-next.config.ts | 4 +- .../gitbook-v2/openNext/incrementalCache.ts | 186 ++++++++++++++++++ packages/gitbook-v2/package.json | 2 +- 5 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 .changeset/clean-crabs-tie.md create mode 100644 packages/gitbook-v2/openNext/incrementalCache.ts diff --git a/.changeset/clean-crabs-tie.md b/.changeset/clean-crabs-tie.md new file mode 100644 index 0000000000..4a618afa39 --- /dev/null +++ b/.changeset/clean-crabs-tie.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +keep data cache in OpenNext between deployment diff --git a/packages/gitbook-v2/.gitignore b/packages/gitbook-v2/.gitignore index 84254823d2..a23d5b7565 100644 --- a/packages/gitbook-v2/.gitignore +++ b/packages/gitbook-v2/.gitignore @@ -6,6 +6,7 @@ # cloudflare .open-next +.wrangler # Symbolic links public diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 4ff4f74e88..060cad541c 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -1,6 +1,4 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare'; -import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache'; -import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache'; import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; import { @@ -9,7 +7,7 @@ import { } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; export default defineCloudflareConfig({ - incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }), + incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), tagCache: withFilter({ tagCache: doShardedTagCache({ baseShardSize: 12, diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts new file mode 100644 index 0000000000..d4a662af01 --- /dev/null +++ b/packages/gitbook-v2/openNext/incrementalCache.ts @@ -0,0 +1,186 @@ +import { createHash } from 'node:crypto'; + +import { trace } from '@/lib/tracing'; +import type { + CacheEntryType, + CacheValue, + IncrementalCache, + WithLastModified, +} from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; + +export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET'; +export const DEFAULT_PREFIX = 'incremental-cache'; + +export type KeyOptions = { + cacheType?: CacheEntryType; +}; + +/** + * + * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace + * the cache operations. It also integrates both R2 and Cache API in a single class. + * Having our own, will allow us to customize it in the future if needed. + */ +class GitbookIncrementalCache implements IncrementalCache { + name = 'GitbookIncrementalCache'; + + protected localCache: Cache | undefined; + + async get<CacheType extends CacheEntryType = 'cache'>( + key: string, + cacheType?: CacheType + ): Promise<WithLastModified<CacheValue<CacheType>> | null> { + const cacheKey = this.getR2Key(key, cacheType); + return trace( + { + operation: 'openNextIncrementalCacheGet', + name: cacheKey, + }, + async (span) => { + span.setAttribute('cacheType', cacheType ?? 'cache'); + const r2 = getCloudflareContext().env[BINDING_NAME]; + const localCache = await this.getCacheInstance(); + if (!r2) throw new Error('No R2 bucket'); + try { + // Check local cache first if available + const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey)); + if (localCacheEntry) { + span.setAttribute('cacheHit', 'local'); + return localCacheEntry.json(); + } + + const r2Object = await r2.get(cacheKey); + if (!r2Object) return null; + + span.setAttribute('cacheHit', 'r2'); + return { + value: await r2Object.json(), + lastModified: r2Object.uploaded.getTime(), + }; + } catch (e) { + console.error('Failed to get from cache', e); + return null; + } + } + ); + } + + async set<CacheType extends CacheEntryType = 'cache'>( + key: string, + value: CacheValue<CacheType>, + cacheType?: CacheType + ): Promise<void> { + const cacheKey = this.getR2Key(key, cacheType); + return trace( + { + operation: 'openNextIncrementalCacheSet', + name: cacheKey, + }, + async (span) => { + span.setAttribute('cacheType', cacheType ?? 'cache'); + const r2 = getCloudflareContext().env[BINDING_NAME]; + const localCache = await this.getCacheInstance(); + if (!r2) throw new Error('No R2 bucket'); + + try { + await r2.put(cacheKey, JSON.stringify(value)); + + //TODO: Check if there is any places where we don't have tags + // Ideally we should always have tags, but in case we don't, we need to decide how to handle it + // For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment + const tags = this.getTagsFromCacheEntry(value) ?? [ + `build_id/${process.env.NEXT_BUILD_ID}`, + ]; + + // We consider R2 as the source of truth, so we update the local cache + // only after a successful R2 write + await localCache.put( + this.getCacheUrlKey(cacheKey), + new Response( + JSON.stringify({ + value, + // Note: `Date.now()` returns the time of the last IO rather than the actual time. + // See https://developers.cloudflare.com/workers/reference/security-model/ + lastModified: Date.now(), + }), + { + headers: { + // Cache-Control default to 30 minutes, will be overridden by `revalidate` + // In theory we should always get the `revalidate` value + 'cache-control': `max-age=${value.revalidate ?? 60 * 30}`, + 'cache-tag': tags.join(','), + }, + } + ) + ); + } catch (e) { + console.error('Failed to set to cache', e); + } + } + ); + } + + async delete(key: string): Promise<void> { + const cacheKey = this.getR2Key(key); + return trace( + { + operation: 'openNextIncrementalCacheDelete', + name: cacheKey, + }, + async () => { + const r2 = getCloudflareContext().env[BINDING_NAME]; + const localCache = await this.getCacheInstance(); + if (!r2) throw new Error('No R2 bucket'); + + try { + await r2.delete(cacheKey); + + // Here again R2 is the source of truth, so we delete from local cache first + await localCache.delete(this.getCacheUrlKey(cacheKey)); + } catch (e) { + console.error('Failed to delete from cache', e); + } + } + ); + } + + async getCacheInstance(): Promise<Cache> { + if (this.localCache) return this.localCache; + this.localCache = await caches.open('incremental-cache'); + return this.localCache; + } + + // Utility function to generate keys for R2/Cache API + getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string { + const hash = createHash('sha256').update(key).digest('hex'); + return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace( + /\/+/g, + '/' + ); + } + + getCacheUrlKey(cacheKey: string): string { + return `http://cache.local/${cacheKey}`; + } + + getTagsFromCacheEntry<CacheType extends CacheEntryType>( + entry: CacheValue<CacheType> + ): string[] | undefined { + if ('tags' in entry && entry.tags) { + return entry.tags; + } + + if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) { + const rawTags = entry.meta.headers['x-next-cache-tags']; + if (typeof rawTags === 'string') { + return rawTags.split(','); + } + } + if ('value' in entry) { + return entry.tags; + } + } +} + +export default new GitbookIncrementalCache(); diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 1db128e211..849aca6b0f 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -29,7 +29,7 @@ "build:v2": "next build", "start": "next start", "build:v2:cloudflare": "opennextjs-cloudflare build", - "dev:v2:cloudflare": "wrangler dev --port 8771", + "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview", "unit": "bun test", "typecheck": "tsc --noEmit" } From 938125e772946438cde884042ea84221420d5a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 19 May 2025 19:10:57 +0200 Subject: [PATCH 049/127] Version Packages (#3180) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-clocks-rush.md | 5 --- .changeset/breezy-bugs-dream.md | 5 --- .changeset/clean-crabs-tie.md | 5 --- .changeset/eight-vans-judge.md | 5 --- .changeset/empty-badgers-happen.md | 5 --- .changeset/fifty-pots-work.md | 5 --- .changeset/four-jobs-reply.md | 5 --- .changeset/giant-dolphins-approve.md | 6 ---- .changeset/healthy-parrots-call.md | 6 ---- .changeset/lemon-actors-sing.md | 6 ---- .changeset/lemon-plants-vanish.md | 8 ----- .changeset/modern-windows-type.md | 5 --- .changeset/moody-maps-chew.md | 5 --- .changeset/polite-falcons-agree.md | 6 ---- .changeset/rare-radios-thank.md | 5 --- .changeset/real-donkeys-invent.md | 5 --- .changeset/rich-eggs-talk.md | 5 --- .changeset/rich-lamps-compare.md | 5 --- .changeset/sharp-falcons-punch.md | 6 ---- .changeset/shy-plums-deny.md | 5 --- .changeset/silly-gorillas-teach.md | 5 --- .changeset/smart-feet-argue.md | 5 --- .changeset/soft-planes-talk.md | 5 --- .changeset/strong-eggs-glow.md | 5 --- .changeset/sweet-schools-flow.md | 6 ---- .changeset/ten-apples-march.md | 5 --- .changeset/thirty-donkeys-help.md | 5 --- .changeset/tidy-rings-talk.md | 6 ---- .changeset/tough-starfishes-grin.md | 5 --- .changeset/twelve-beers-confess.md | 5 --- .changeset/twelve-news-joke.md | 5 --- .changeset/two-chairs-wait.md | 5 --- .changeset/two-lions-tickle.md | 6 ---- .changeset/young-games-talk.md | 6 ---- .changeset/young-poets-cry.md | 5 --- packages/colors/CHANGELOG.md | 8 +++++ packages/colors/package.json | 2 +- packages/gitbook-v2/CHANGELOG.md | 19 ++++++++++++ packages/gitbook-v2/package.json | 2 +- packages/gitbook/CHANGELOG.md | 46 ++++++++++++++++++++++++++++ packages/gitbook/package.json | 2 +- packages/openapi-parser/CHANGELOG.md | 6 ++++ packages/openapi-parser/package.json | 2 +- packages/react-openapi/CHANGELOG.md | 19 ++++++++++++ packages/react-openapi/package.json | 2 +- 45 files changed, 103 insertions(+), 192 deletions(-) delete mode 100644 .changeset/big-clocks-rush.md delete mode 100644 .changeset/breezy-bugs-dream.md delete mode 100644 .changeset/clean-crabs-tie.md delete mode 100644 .changeset/eight-vans-judge.md delete mode 100644 .changeset/empty-badgers-happen.md delete mode 100644 .changeset/fifty-pots-work.md delete mode 100644 .changeset/four-jobs-reply.md delete mode 100644 .changeset/giant-dolphins-approve.md delete mode 100644 .changeset/healthy-parrots-call.md delete mode 100644 .changeset/lemon-actors-sing.md delete mode 100644 .changeset/lemon-plants-vanish.md delete mode 100644 .changeset/modern-windows-type.md delete mode 100644 .changeset/moody-maps-chew.md delete mode 100644 .changeset/polite-falcons-agree.md delete mode 100644 .changeset/rare-radios-thank.md delete mode 100644 .changeset/real-donkeys-invent.md delete mode 100644 .changeset/rich-eggs-talk.md delete mode 100644 .changeset/rich-lamps-compare.md delete mode 100644 .changeset/sharp-falcons-punch.md delete mode 100644 .changeset/shy-plums-deny.md delete mode 100644 .changeset/silly-gorillas-teach.md delete mode 100644 .changeset/smart-feet-argue.md delete mode 100644 .changeset/soft-planes-talk.md delete mode 100644 .changeset/strong-eggs-glow.md delete mode 100644 .changeset/sweet-schools-flow.md delete mode 100644 .changeset/ten-apples-march.md delete mode 100644 .changeset/thirty-donkeys-help.md delete mode 100644 .changeset/tidy-rings-talk.md delete mode 100644 .changeset/tough-starfishes-grin.md delete mode 100644 .changeset/twelve-beers-confess.md delete mode 100644 .changeset/twelve-news-joke.md delete mode 100644 .changeset/two-chairs-wait.md delete mode 100644 .changeset/two-lions-tickle.md delete mode 100644 .changeset/young-games-talk.md delete mode 100644 .changeset/young-poets-cry.md diff --git a/.changeset/big-clocks-rush.md b/.changeset/big-clocks-rush.md deleted file mode 100644 index 665953599f..0000000000 --- a/.changeset/big-clocks-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": minor ---- - -Fix images in reusable content across spaces. diff --git a/.changeset/breezy-bugs-dream.md b/.changeset/breezy-bugs-dream.md deleted file mode 100644 index 69d2022873..0000000000 --- a/.changeset/breezy-bugs-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'gitbook': patch ---- - -Fix openapi-select hover in responses diff --git a/.changeset/clean-crabs-tie.md b/.changeset/clean-crabs-tie.md deleted file mode 100644 index 4a618afa39..0000000000 --- a/.changeset/clean-crabs-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook-v2": patch ---- - -keep data cache in OpenNext between deployment diff --git a/.changeset/eight-vans-judge.md b/.changeset/eight-vans-judge.md deleted file mode 100644 index c432fde1a3..0000000000 --- a/.changeset/eight-vans-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@gitbook/colors": patch ---- - -Update chroma ratio per step diff --git a/.changeset/empty-badgers-happen.md b/.changeset/empty-badgers-happen.md deleted file mode 100644 index 75eaad7165..0000000000 --- a/.changeset/empty-badgers-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook-v2": patch ---- - -Only resize images with supported extensions. diff --git a/.changeset/fifty-pots-work.md b/.changeset/fifty-pots-work.md deleted file mode 100644 index 0ba6550278..0000000000 --- a/.changeset/fifty-pots-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Decrease rounding of header logo diff --git a/.changeset/four-jobs-reply.md b/.changeset/four-jobs-reply.md deleted file mode 100644 index 714b8deef3..0000000000 --- a/.changeset/four-jobs-reply.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@gitbook/openapi-parser': patch ---- - -Pass scalar's errors through OpenAPIParseError diff --git a/.changeset/giant-dolphins-approve.md b/.changeset/giant-dolphins-approve.md deleted file mode 100644 index 0fa9d77174..0000000000 --- a/.changeset/giant-dolphins-approve.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@gitbook/react-openapi': patch -'gitbook': patch ---- - -Fix openapi CR preview diff --git a/.changeset/healthy-parrots-call.md b/.changeset/healthy-parrots-call.md deleted file mode 100644 index 9503e2afb1..0000000000 --- a/.changeset/healthy-parrots-call.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook-v2": patch -"gitbook": patch ---- - -Fix URL in sitemap diff --git a/.changeset/lemon-actors-sing.md b/.changeset/lemon-actors-sing.md deleted file mode 100644 index d212947861..0000000000 --- a/.changeset/lemon-actors-sing.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@gitbook/react-openapi": minor -"gitbook": minor ---- - -Design tweaks to code blocks and OpenAPI pages diff --git a/.changeset/lemon-plants-vanish.md b/.changeset/lemon-plants-vanish.md deleted file mode 100644 index 0887b10bef..0000000000 --- a/.changeset/lemon-plants-vanish.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@gitbook/react-openapi": patch -"gitbook-v2": patch -"gitbook": patch -"@gitbook/colors": patch ---- - -Fix code highlighting for HTTP diff --git a/.changeset/modern-windows-type.md b/.changeset/modern-windows-type.md deleted file mode 100644 index d2135d1c28..0000000000 --- a/.changeset/modern-windows-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook-v2": patch ---- - -Fix crash on Cloudflare by using latest stable version of Next.js instead of canary diff --git a/.changeset/moody-maps-chew.md b/.changeset/moody-maps-chew.md deleted file mode 100644 index 0c06afc2ad..0000000000 --- a/.changeset/moody-maps-chew.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Fix resolution of links in reusable contents diff --git a/.changeset/polite-falcons-agree.md b/.changeset/polite-falcons-agree.md deleted file mode 100644 index 7743ab7095..0000000000 --- a/.changeset/polite-falcons-agree.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook-v2": minor -"gitbook": minor ---- - -Add support for reusable content across spaces. diff --git a/.changeset/rare-radios-thank.md b/.changeset/rare-radios-thank.md deleted file mode 100644 index 0fb143ce42..0000000000 --- a/.changeset/rare-radios-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Better print layouts: wrap code blocks & force table column auto-sizing diff --git a/.changeset/real-donkeys-invent.md b/.changeset/real-donkeys-invent.md deleted file mode 100644 index a322af7d72..0000000000 --- a/.changeset/real-donkeys-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Fix invalid sitemap.xml generated with relative URLs instead of absolute ones diff --git a/.changeset/rich-eggs-talk.md b/.changeset/rich-eggs-talk.md deleted file mode 100644 index e863378b55..0000000000 --- a/.changeset/rich-eggs-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Change OpenAPI schema-optional from `info` to `tint` color diff --git a/.changeset/rich-lamps-compare.md b/.changeset/rich-lamps-compare.md deleted file mode 100644 index 57b026b7b4..0000000000 --- a/.changeset/rich-lamps-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@gitbook/colors": patch ---- - -Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast diff --git a/.changeset/sharp-falcons-punch.md b/.changeset/sharp-falcons-punch.md deleted file mode 100644 index fbcb613276..0000000000 --- a/.changeset/sharp-falcons-punch.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@gitbook/react-openapi': patch -'gitbook': patch ---- - -Fix schemas disclosure label causing client error diff --git a/.changeset/shy-plums-deny.md b/.changeset/shy-plums-deny.md deleted file mode 100644 index 22193b58e1..0000000000 --- a/.changeset/shy-plums-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@gitbook/react-openapi': patch ---- - -Missing top-level required OpenAPI alternatives diff --git a/.changeset/silly-gorillas-teach.md b/.changeset/silly-gorillas-teach.md deleted file mode 100644 index b5691ce68a..0000000000 --- a/.changeset/silly-gorillas-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@gitbook/react-openapi': patch ---- - -Handle OpenAPI alternatives from schema.items diff --git a/.changeset/smart-feet-argue.md b/.changeset/smart-feet-argue.md deleted file mode 100644 index 079dada9b0..0000000000 --- a/.changeset/smart-feet-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@gitbook/react-openapi': patch ---- - -Handle invalid OpenAPI Responses diff --git a/.changeset/soft-planes-talk.md b/.changeset/soft-planes-talk.md deleted file mode 100644 index d633d90dcd..0000000000 --- a/.changeset/soft-planes-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook-v2": patch ---- - -Improve error messages around undefined site sections. diff --git a/.changeset/strong-eggs-glow.md b/.changeset/strong-eggs-glow.md deleted file mode 100644 index 7c1e1db039..0000000000 --- a/.changeset/strong-eggs-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'gitbook': patch ---- - -Fix OpenAPI layout issues diff --git a/.changeset/sweet-schools-flow.md b/.changeset/sweet-schools-flow.md deleted file mode 100644 index 6ff44bfa96..0000000000 --- a/.changeset/sweet-schools-flow.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook-v2": patch -"gitbook": patch ---- - -Increase logging around caching behaviour causing page crashes. diff --git a/.changeset/ten-apples-march.md b/.changeset/ten-apples-march.md deleted file mode 100644 index dd13b276c3..0000000000 --- a/.changeset/ten-apples-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@gitbook/react-openapi': patch ---- - -Use default value if string number or boolean in generateSchemaExample diff --git a/.changeset/thirty-donkeys-help.md b/.changeset/thirty-donkeys-help.md deleted file mode 100644 index 529b8a9024..0000000000 --- a/.changeset/thirty-donkeys-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Prevent section group popovers from opening on click diff --git a/.changeset/tidy-rings-talk.md b/.changeset/tidy-rings-talk.md deleted file mode 100644 index 75ac0e7f8d..0000000000 --- a/.changeset/tidy-rings-talk.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook": patch -"gitbook-v2": patch ---- - -Update the regex for validating site redirect diff --git a/.changeset/tough-starfishes-grin.md b/.changeset/tough-starfishes-grin.md deleted file mode 100644 index 2f150b59d8..0000000000 --- a/.changeset/tough-starfishes-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Always link main logo to the root of the site diff --git a/.changeset/twelve-beers-confess.md b/.changeset/twelve-beers-confess.md deleted file mode 100644 index 1c70f6880b..0000000000 --- a/.changeset/twelve-beers-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Change `Dropdown`s to use Radix's `DropdownMenu` diff --git a/.changeset/twelve-news-joke.md b/.changeset/twelve-news-joke.md deleted file mode 100644 index 3c0d08681f..0000000000 --- a/.changeset/twelve-news-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@gitbook/react-openapi": patch ---- - -Fix Python code sample "null vs None" diff --git a/.changeset/two-chairs-wait.md b/.changeset/two-chairs-wait.md deleted file mode 100644 index 6ca7941574..0000000000 --- a/.changeset/two-chairs-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Add border to filled sidebar on gradient theme diff --git a/.changeset/two-lions-tickle.md b/.changeset/two-lions-tickle.md deleted file mode 100644 index b9fa676033..0000000000 --- a/.changeset/two-lions-tickle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook-v2": minor -"gitbook": minor ---- - -Pass SVG images through image resizing without resizing them to serve them from optimal host. diff --git a/.changeset/young-games-talk.md b/.changeset/young-games-talk.md deleted file mode 100644 index 096dc02060..0000000000 --- a/.changeset/young-games-talk.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"gitbook": patch -"gitbook-v2": patch ---- - -Revert investigation work around URL caches. diff --git a/.changeset/young-poets-cry.md b/.changeset/young-poets-cry.md deleted file mode 100644 index 8757b729b9..0000000000 --- a/.changeset/young-poets-cry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gitbook": patch ---- - -Fix OpenAPI disclosure label ("Show properties") misalignment on mobile diff --git a/packages/colors/CHANGELOG.md b/packages/colors/CHANGELOG.md index 0c1606b101..1695a6c97d 100644 --- a/packages/colors/CHANGELOG.md +++ b/packages/colors/CHANGELOG.md @@ -1,5 +1,13 @@ # @gitbook/colors +## 0.3.3 + +### Patch Changes + +- c3f6b8c: Update chroma ratio per step +- 5e975ab: Fix code highlighting for HTTP +- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast + ## 0.3.2 ### Patch Changes diff --git a/packages/colors/package.json b/packages/colors/package.json index 36cd0f47f8..16f54d1ff3 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -8,7 +8,7 @@ "default": "./dist/index.js" } }, - "version": "0.3.2", + "version": "0.3.3", "devDependencies": { "typescript": "^5.5.3" }, diff --git a/packages/gitbook-v2/CHANGELOG.md b/packages/gitbook-v2/CHANGELOG.md index eb4e718a5b..8a4a4a0288 100644 --- a/packages/gitbook-v2/CHANGELOG.md +++ b/packages/gitbook-v2/CHANGELOG.md @@ -1,5 +1,24 @@ # gitbook-v2 +## 0.3.0 + +### Minor Changes + +- 3119066: Add support for reusable content across spaces. +- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host. + +### Patch Changes + +- 1c8d9fe: keep data cache in OpenNext between deployment +- 778624a: Only resize images with supported extensions. +- e6ddc0f: Fix URL in sitemap +- 5e975ab: Fix code highlighting for HTTP +- e15757d: Fix crash on Cloudflare by using latest stable version of Next.js instead of canary +- 634e0b4: Improve error messages around undefined site sections. +- 97b7c79: Increase logging around caching behaviour causing page crashes. +- 3f29206: Update the regex for validating site redirect +- dd043df: Revert investigation work around URL caches. + ## 0.2.5 ### Patch Changes diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 849aca6b0f..355518c4f6 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -1,6 +1,6 @@ { "name": "gitbook-v2", - "version": "0.2.5", + "version": "0.3.0", "private": true, "dependencies": { "@gitbook/api": "^0.115.0", diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md index 21003be437..fb855a6305 100644 --- a/packages/gitbook/CHANGELOG.md +++ b/packages/gitbook/CHANGELOG.md @@ -1,5 +1,51 @@ # gitbook +## 0.12.0 + +### Minor Changes + +- 8339e91: Fix images in reusable content across spaces. +- 326e28e: Design tweaks to code blocks and OpenAPI pages +- 3119066: Add support for reusable content across spaces. +- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host. + +### Patch Changes + +- c4ebb3f: Fix openapi-select hover in responses +- aed79fd: Decrease rounding of header logo +- 42ca7e1: Fix openapi CR preview +- e6ddc0f: Fix URL in sitemap +- 5e975ab: Fix code highlighting for HTTP +- 5d504ff: Fix resolution of links in reusable contents +- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing +- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones +- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color +- 580101d: Fix schemas disclosure label causing client error +- 12a455d: Fix OpenAPI layout issues +- 97b7c79: Increase logging around caching behaviour causing page crashes. +- 373f18f: Prevent section group popovers from opening on click +- 3f29206: Update the regex for validating site redirect +- 0c973a3: Always link main logo to the root of the site +- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu` +- 0e201d5: Add border to filled sidebar on gradient theme +- dd043df: Revert investigation work around URL caches. +- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile +- Updated dependencies [c3f6b8c] +- Updated dependencies [d00dc8c] +- Updated dependencies [42ca7e1] +- Updated dependencies [326e28e] +- Updated dependencies [5e975ab] +- Updated dependencies [f7a3470] +- Updated dependencies [580101d] +- Updated dependencies [20ebecb] +- Updated dependencies [80cb52a] +- Updated dependencies [cb5598d] +- Updated dependencies [c6637b0] +- Updated dependencies [a3ec264] + - @gitbook/colors@0.3.3 + - @gitbook/openapi-parser@2.1.4 + - @gitbook/react-openapi@1.3.0 + ## 0.11.1 ### Patch Changes diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 4294327546..a3b9da821a 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -1,6 +1,6 @@ { "name": "gitbook", - "version": "0.11.1", + "version": "0.12.0", "private": true, "scripts": { "dev": "env-cmd --silent -f ../../.env.local next dev", diff --git a/packages/openapi-parser/CHANGELOG.md b/packages/openapi-parser/CHANGELOG.md index 36f5cbc224..7f6ac7ccd2 100644 --- a/packages/openapi-parser/CHANGELOG.md +++ b/packages/openapi-parser/CHANGELOG.md @@ -1,5 +1,11 @@ # @gitbook/openapi-parser +## 2.1.4 + +### Patch Changes + +- d00dc8c: Pass scalar's errors through OpenAPIParseError + ## 2.1.3 ### Patch Changes diff --git a/packages/openapi-parser/package.json b/packages/openapi-parser/package.json index f0829fca1b..e65ebb4dda 100644 --- a/packages/openapi-parser/package.json +++ b/packages/openapi-parser/package.json @@ -9,7 +9,7 @@ "default": "./dist/index.js" } }, - "version": "2.1.3", + "version": "2.1.4", "sideEffects": false, "dependencies": { "@scalar/openapi-parser": "^0.10.10", diff --git a/packages/react-openapi/CHANGELOG.md b/packages/react-openapi/CHANGELOG.md index f74c6f044b..6f36c7dc61 100644 --- a/packages/react-openapi/CHANGELOG.md +++ b/packages/react-openapi/CHANGELOG.md @@ -1,5 +1,24 @@ # @gitbook/react-openapi +## 1.3.0 + +### Minor Changes + +- 326e28e: Design tweaks to code blocks and OpenAPI pages + +### Patch Changes + +- 42ca7e1: Fix openapi CR preview +- 5e975ab: Fix code highlighting for HTTP +- 580101d: Fix schemas disclosure label causing client error +- 20ebecb: Missing top-level required OpenAPI alternatives +- 80cb52a: Handle OpenAPI alternatives from schema.items +- cb5598d: Handle invalid OpenAPI Responses +- c6637b0: Use default value if string number or boolean in generateSchemaExample +- a3ec264: Fix Python code sample "null vs None" +- Updated dependencies [d00dc8c] + - @gitbook/openapi-parser@2.1.4 + ## 1.2.1 ### Patch Changes diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index cd1099c242..d4f170270a 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -8,7 +8,7 @@ "default": "./dist/index.js" } }, - "version": "1.2.1", + "version": "1.3.0", "sideEffects": false, "dependencies": { "@gitbook/openapi-parser": "workspace:*", From 4b67fe5317962f4fec17fc4f4e6b24f3073015c8 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 20 May 2025 10:25:35 +0200 Subject: [PATCH 050/127] V2: Add `urlObject.hash` to `linker.toLinkForContent` (#3246) --- .changeset/thin-buckets-grow.md | 5 +++++ packages/gitbook-v2/src/lib/context.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/thin-buckets-grow.md diff --git a/.changeset/thin-buckets-grow.md b/.changeset/thin-buckets-grow.md new file mode 100644 index 0000000000..84375c735d --- /dev/null +++ b/.changeset/thin-buckets-grow.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index 22e25a5990..ba0f299574 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -165,7 +165,7 @@ export function getBaseContext(input: { // Create link in the same format for links to other sites/sections. linker.toLinkForContent = (rawURL: string) => { const urlObject = new URL(rawURL); - return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`; + return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`; }; } From 57bb146075e768a7dcb4e6edb6a3bd4be0bb2cd0 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 20 May 2025 14:13:57 +0200 Subject: [PATCH 051/127] Dynamic TOC height (#3233) --- .changeset/slow-lizards-obey.md | 5 + .../Announcement/AnnouncementBanner.tsx | 2 +- .../gitbook/src/components/Footer/Footer.tsx | 1 + .../RootLayout/CustomizationRootLayout.tsx | 2 +- .../src/components/RootLayout/globals.css | 2 +- .../TableOfContents/TableOfContents.tsx | 165 ++++++++++-------- .../TableOfContents/TableOfContentsScript.tsx | 73 ++++++++ .../src/components/TableOfContents/index.ts | 5 +- packages/gitbook/tailwind.config.ts | 6 +- 9 files changed, 179 insertions(+), 82 deletions(-) create mode 100644 .changeset/slow-lizards-obey.md create mode 100644 packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx diff --git a/.changeset/slow-lizards-obey.md b/.changeset/slow-lizards-obey.md new file mode 100644 index 0000000000..ce1ef0e04c --- /dev/null +++ b/.changeset/slow-lizards-obey.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Make TOC height dynamic based on visible header and footer elements diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx index c89bb24804..522bde0ca3 100644 --- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx +++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx @@ -25,7 +25,7 @@ export function AnnouncementBanner(props: { const style = BANNER_STYLES[announcement.style]; return ( - <div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2"> + <div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2"> <div className="scroll-nojump"> <div className={tcls('relative', CONTAINER_STYLE)}> <Tag diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx index 776c0a4af4..5723d67f3c 100644 --- a/packages/gitbook/src/components/Footer/Footer.tsx +++ b/packages/gitbook/src/components/Footer/Footer.tsx @@ -25,6 +25,7 @@ export function Footer(props: { context: GitBookSiteContext }) { return ( <footer + id="site-footer" className={tcls( 'border-tint-subtle border-t', // If the footer only contains a mode toggle, we only show it on smaller screens diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx index c7d2f35b6e..c6f141425a 100644 --- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx +++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx @@ -75,7 +75,7 @@ export async function CustomizationRootLayout(props: { lang={customization.internationalization.locale} className={tcls( customization.header.preset === CustomizationHeaderPreset.None - ? 'site-header-none' + ? null : 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling customization.styling.corners === CustomizationCorners.Straight ? ' straight-corners' diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index 71dae7cc6b..15c09614e3 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -161,6 +161,6 @@ html.dark { color-scheme: dark light; } -html.announcement-hidden .announcement-banner { +html.announcement-hidden #announcement-banner { @apply hidden; } diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index dcfb287660..7a6993fde1 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -6,6 +6,7 @@ import { tcls } from '@/lib/tailwind'; import { PagesList } from './PagesList'; import { TOCScrollContainer } from './TOCScroller'; +import { TableOfContentsScript } from './TableOfContentsScript'; import { Trademark } from './Trademark'; export function TableOfContents(props: { @@ -17,97 +18,107 @@ export function TableOfContents(props: { const { space, customization, pages } = context; return ( - <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar. - data-testid="table-of-contents" - className={tcls( - 'group', - 'text-sm', + <> + <aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar. + data-testid="table-of-contents" + id="table-of-contents" + className={tcls( + 'group', + 'text-sm', - 'grow-0', - 'shrink-0', - 'basis-full', - 'lg:basis-72', - 'page-no-toc:lg:basis-56', + 'grow-0', + 'shrink-0', + 'basis-full', + 'lg:basis-72', + 'page-no-toc:lg:basis-56', - 'relative', - 'z-[1]', - 'lg:sticky', - // Without header - 'lg:top-0', - 'lg:h-screen', + 'relative', + 'z-[1]', + 'lg:sticky', - // With header - 'site-header:lg:top-16', - 'site-header:lg:h-[calc(100vh_-_4rem)]', + // Server-side static positioning + 'lg:top-0', + 'lg:h-screen', + 'announcement:lg:h-[calc(100vh-4.25rem)]', - // With header and sections - 'site-header-sections:lg:top-[6.75rem]', - 'site-header-sections:lg:h-[calc(100vh_-_6.75rem)]', + 'site-header:lg:top-16', + 'site-header:lg:h-[calc(100vh-4rem)]', + 'announcement:site-header:lg:h-[calc(100vh-4rem-4.25rem)]', - 'pt-6', - 'pb-4', - 'sidebar-filled:lg:pr-6', - 'page-no-toc:lg:pr-0', + 'site-header-sections:lg:top-[6.75rem]', + 'site-header-sections:lg:h-[calc(100vh-6.75rem)]', + 'announcement:site-header-sections:lg:h-[calc(100vh-6.75rem-4.25rem)]', - 'hidden', - 'navigation-open:!flex', - 'lg:flex', - 'page-no-toc:lg:hidden', - 'page-no-toc:xl:flex', - 'site-header-none:page-no-toc:lg:flex', - 'flex-col', - 'gap-4', + // Client-side dynamic positioning (CSS vars applied by script) + '[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]', + '[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]', - 'navigation-open:border-b', - 'border-tint-subtle' - )} - > - {header && header} - <div // The actual sidebar, either shown with a filled bg or transparent. - className={tcls( - 'lg:-ms-5', - 'relative flex flex-grow flex-col overflow-hidden border-tint-subtle', + 'pt-6', + 'pb-4', + 'sidebar-filled:lg:pr-6', + 'page-no-toc:lg:pr-0', - 'sidebar-filled:bg-tint-subtle', - 'theme-muted:bg-tint-subtle', - '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle', - '[html.sidebar-filled.theme-muted_&]:bg-tint-base', - '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', - '[html.sidebar-filled.theme-gradient_&]:border', - 'page-no-toc:!bg-transparent', + 'hidden', + 'navigation-open:!flex', + 'lg:flex', + 'page-no-toc:lg:hidden', + 'page-no-toc:xl:flex', + 'site-header-none:page-no-toc:lg:flex', + 'flex-col', + 'gap-4', - 'sidebar-filled:rounded-xl', - 'straight-corners:rounded-none' + 'navigation-open:border-b', + 'border-tint-subtle' )} > - {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>} - <TOCScrollContainer // The scrollview inside the sidebar + {header && header} + <div // The actual sidebar, either shown with a filled bg or transparent. className={tcls( - 'flex flex-grow flex-col p-2', - customization.trademark.enabled && 'lg:pb-20', - 'lg:gutter-stable overflow-y-auto', - '[&::-webkit-scrollbar]:bg-transparent', - '[&::-webkit-scrollbar-thumb]:bg-transparent', - 'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle', - 'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7', - 'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8' + 'lg:-ms-5', + 'relative flex flex-grow flex-col overflow-hidden border-tint-subtle', + + 'sidebar-filled:bg-tint-subtle', + 'theme-muted:bg-tint-subtle', + '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle', + '[html.sidebar-filled.theme-muted_&]:bg-tint-base', + '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', + '[html.sidebar-filled.theme-gradient_&]:border', + 'page-no-toc:!bg-transparent', + + 'sidebar-filled:rounded-xl', + 'straight-corners:rounded-none' )} > - <PagesList - rootPages={pages} - pages={pages} - context={context} - style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l" - /> - {customization.trademark.enabled ? ( - <Trademark - space={space} - customization={customization} - placement={SiteInsightsTrademarkPlacement.Sidebar} + {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>} + <TOCScrollContainer // The scrollview inside the sidebar + className={tcls( + 'flex flex-grow flex-col p-2', + customization.trademark.enabled && 'lg:pb-20', + 'lg:gutter-stable overflow-y-auto', + '[&::-webkit-scrollbar]:bg-transparent', + '[&::-webkit-scrollbar-thumb]:bg-transparent', + 'group-hover:[&::-webkit-scrollbar]:bg-tint-subtle', + 'group-hover:[&::-webkit-scrollbar-thumb]:bg-tint-7', + 'group-hover:[&::-webkit-scrollbar-thumb:hover]:bg-tint-8' + )} + > + <PagesList + rootPages={pages} + pages={pages} + context={context} + style="page-no-toc:hidden border-tint-subtle sidebar-list-line:border-l" /> - ) : null} - </TOCScrollContainer> - </div> - </aside> + {customization.trademark.enabled ? ( + <Trademark + space={space} + customization={customization} + placement={SiteInsightsTrademarkPlacement.Sidebar} + /> + ) : null} + </TOCScrollContainer> + </div> + </aside> + <TableOfContentsScript /> + </> ); } diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx new file mode 100644 index 0000000000..a4e6bea18c --- /dev/null +++ b/packages/gitbook/src/components/TableOfContents/TableOfContentsScript.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { useEffect } from 'react'; + +/** + * Adjusts TableOfContents height based on visible elements + */ +export function TableOfContentsScript() { + useEffect(() => { + const root = document.documentElement; + + // Calculate and set TOC dimensions + const updateTocLayout = () => { + // Get key elements + const header = document.getElementById('site-header'); + const banner = document.getElementById('announcement-banner'); + const footer = document.getElementById('site-footer'); + + // Set sticky top position based on header + const headerHeight = header?.offsetHeight ?? 0; + root.style.setProperty('--toc-top-offset', `${headerHeight}px`); + + // Start with full viewport height minus header + let height = window.innerHeight - headerHeight; + + // Subtract visible banner (if any) + if (banner && window.getComputedStyle(banner).display !== 'none') { + const bannerRect = banner.getBoundingClientRect(); + if (bannerRect.height > 0 && bannerRect.bottom > 0) { + height -= Math.min(bannerRect.height, bannerRect.bottom); + } + } + + // Subtract visible footer (if any) + if (footer) { + const footerRect = footer.getBoundingClientRect(); + if (footerRect.top < window.innerHeight) { + height -= Math.min(footerRect.height, window.innerHeight - footerRect.top); + } + } + + // Update height + root.style.setProperty('--toc-height', `${height}px`); + }; + + // Initial update + updateTocLayout(); + + // Let the browser handle scroll throttling naturally + window.addEventListener('scroll', updateTocLayout, { passive: true }); + window.addEventListener('resize', updateTocLayout, { passive: true }); + + // Use MutationObserver for DOM changes + const observer = new MutationObserver(() => { + requestAnimationFrame(updateTocLayout); + }); + + // Only observe what matters + observer.observe(document.documentElement, { + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'], + }); + + return () => { + observer.disconnect(); + window.removeEventListener('scroll', updateTocLayout); + window.removeEventListener('resize', updateTocLayout); + }; + }, []); + + return null; +} diff --git a/packages/gitbook/src/components/TableOfContents/index.ts b/packages/gitbook/src/components/TableOfContents/index.ts index 715a3a239e..6eeff92697 100644 --- a/packages/gitbook/src/components/TableOfContents/index.ts +++ b/packages/gitbook/src/components/TableOfContents/index.ts @@ -1 +1,4 @@ -export * from './TableOfContents'; +export { TableOfContents } from './TableOfContents'; +export { PagesList } from './PagesList'; +export { TOCScrollContainer } from './TOCScroller'; +export { Trademark } from './Trademark'; diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 2e5b7c359f..9d248c9a40 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -458,12 +458,16 @@ const config: Config = { /** * Variant when a header is displayed. */ - addVariant('site-header-none', 'html.site-header-none &'); + addVariant('site-header-none', 'body:not(:has(#site-header:not(.mobile-only))) &'); addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &'); addVariant('site-header-sections', [ 'body:has(#site-header:not(.mobile-only) #sections) &', 'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &', ]); + addVariant( + 'announcement', + 'html:not(.announcement-hidden):has(#announcement-banner) &' + ); const customisationVariants = { // Sidebar styles From d88dd5ed8af091bae7947ac4ecc8670592bcd2b4 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Tue, 20 May 2025 19:04:07 +0200 Subject: [PATCH 052/127] Update OpenNext (#3248) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- bun.lock | 68 ++++++++++++++++-------------- packages/gitbook-v2/package.json | 2 +- packages/gitbook-v2/wrangler.jsonc | 6 +-- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/bun.lock b/bun.lock index 2c13245d05..631adb8ce4 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ }, "packages/colors": { "name": "@gitbook/colors", - "version": "0.3.2", + "version": "0.3.3", "devDependencies": { "typescript": "^5.5.3", }, @@ -49,7 +49,7 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.11.1", + "version": "0.12.0", "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-do": "workspace:*", @@ -141,11 +141,11 @@ }, "packages/gitbook-v2": { "name": "gitbook-v2", - "version": "0.2.5", + "version": "0.3.0", "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.0.3", + "@opennextjs/cloudflare": "1.0.4", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -185,7 +185,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "2.1.3", + "version": "2.1.4", "dependencies": { "@scalar/openapi-parser": "^0.10.10", "@scalar/openapi-types": "^0.1.9", @@ -232,7 +232,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.2.1", + "version": "1.3.0", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "^1.2.19", @@ -791,9 +791,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opennextjs/aws": ["@opennextjs/aws@3.6.1", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-RYU9K58vEUPXqc3pZO6kr9vBy1MmJZFQZLe0oXBskC005oGju/m4e3DCCP4eZ/Q/HdYQXCoqNXgSGi8VCAYgew=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.1", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-+SNhz2JmKkMlWzndTSYZeq0eseAPfEXUUYJA7H9MkgFe36i1no0OdiLuwu9e4uR3yuOzaNWB+skCYHsD6Im9nA=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -3649,7 +3649,7 @@ "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="], + "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -4721,49 +4721,55 @@ "@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="], + "@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], - "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="], + "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="], - "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="], + "@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="], - "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="], + "@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="], - "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="], + "@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="], - "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="], + "@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="], - "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="], + "@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="], - "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="], + "@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="], - "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="], + "@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="], - "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="], + "@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="], - "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="], + "@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="], - "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="], + "@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="], - "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="], + "@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="], - "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="], + "@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="], - "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="], + "@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="], - "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="], + "@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="], - "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="], + "@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="], - "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="], + "@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="], - "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="], + "@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="], - "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="], + "@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="], - "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="], + "@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="], - "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="], + "@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="], + + "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], + + "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + + "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 355518c4f6..5561b7f8d3 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,7 +5,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.0.3", + "@opennextjs/cloudflare": "1.0.4", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index 6d8ccf9955..8d420d3b2d 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -91,9 +91,9 @@ }, "production": { "vars": { - // This is a bit misleading, but it means that we can have 100 concurrent revalidations - // This means that we'll have up to 20 durable objects instance running at the same time - "MAX_REVALIDATE_CONCURRENCY": "20" + // This is a bit misleading, but it means that we can have 500 concurrent revalidations + // This means that we'll have up to 100 durable objects instance running at the same time + "MAX_REVALIDATE_CONCURRENCY": "100" }, "routes": [ { From b259009c9c94e1fb76c11ee40bc99650266a2f87 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Tue, 20 May 2025 19:33:33 +0200 Subject: [PATCH 053/127] Add temporary logging for Cloudflare debugging (#3249) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- packages/gitbook-v2/wrangler.jsonc | 5 ++++- packages/gitbook/src/routes/ogimage.tsx | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index 8d420d3b2d..03fbcec65b 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -93,7 +93,10 @@ "vars": { // This is a bit misleading, but it means that we can have 500 concurrent revalidations // This means that we'll have up to 100 durable objects instance running at the same time - "MAX_REVALIDATE_CONCURRENCY": "100" + "MAX_REVALIDATE_CONCURRENCY": "100", + // Temporary variable to find the issue once deployed + // TODO: remove this once the issue is fixed + "DEBUG_CLOUDFLARE": "true" }, "routes": [ { diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 41842ab241..c1cfd97723 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -311,6 +311,17 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { }; } +/** + * Temporary function to log some data on Cloudflare. + * TODO: remove this when we found the issue + */ +function logOnCloudflareOnly(message: string) { + if (process.env.DEBUG_CLOUDFLARE === 'true') { + // biome-ignore lint/suspicious/noConsole: <explanation> + console.log(message); + } +} + /** * Fetch a resource from the function itself. * To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding. @@ -318,6 +329,7 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { async function fetchSelf(url: string) { const cloudflare = getCloudflareContext(); if (cloudflare?.env.WORKER_SELF_REFERENCE) { + logOnCloudflareOnly(`Fetching self: ${url}`); return await cloudflare.env.WORKER_SELF_REFERENCE.fetch( // `getAssetURL` can return a relative URL, so we need to make it absolute // the URL doesn't matter, as we're using the worker-self-reference binding @@ -334,6 +346,9 @@ async function fetchSelf(url: string) { async function readImage(response: Response) { const contentType = response.headers.get('content-type'); if (!contentType || !contentType.startsWith('image/')) { + logOnCloudflareOnly(`Invalid content type: ${contentType}, + status: ${response.status} + rayId: ${response.headers.get('cf-ray')}`); throw new Error(`Invalid content type: ${contentType}`); } From ba0094a862e28b83728cc05c2503c4b652cf93ed Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 21 May 2025 09:52:05 +0200 Subject: [PATCH 054/127] fix ISR in preview in cloudflare (#3250) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/nasty-moles-visit.md | 5 +++++ packages/gitbook-v2/open-next.config.ts | 3 +-- packages/gitbook-v2/openNext/queue.ts | 17 +++++++++++++++++ packages/gitbook-v2/wrangler.jsonc | 6 +++++- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .changeset/nasty-moles-visit.md create mode 100644 packages/gitbook-v2/openNext/queue.ts diff --git a/.changeset/nasty-moles-visit.md b/.changeset/nasty-moles-visit.md new file mode 100644 index 0000000000..031fb81e30 --- /dev/null +++ b/.changeset/nasty-moles-visit.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +fix ISR on preview env diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 060cad541c..d35c9ef03e 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -1,5 +1,4 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare'; -import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; import { softTagFilter, @@ -20,7 +19,7 @@ export default defineCloudflareConfig({ // We don't use `revalidatePath`, so we filter out soft tags filterFn: softTagFilter, }), - queue: doQueue, + queue: () => import('./openNext/queue').then((m) => m.default), // Performance improvements as we don't use PPR enableCacheInterception: true, diff --git a/packages/gitbook-v2/openNext/queue.ts b/packages/gitbook-v2/openNext/queue.ts new file mode 100644 index 0000000000..ab33c479d9 --- /dev/null +++ b/packages/gitbook-v2/openNext/queue.ts @@ -0,0 +1,17 @@ +import type { Queue } from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; +import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; +import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue'; + +interface Env { + IS_PREVIEW?: string; +} + +export default { + name: 'GitbookISRQueue', + send: async (msg) => { + const { ctx, env } = getCloudflareContext(); + const isPreview = (env as Env).IS_PREVIEW === 'true'; + ctx.waitUntil(isPreview ? memoryQueue.send(msg) : doQueue.send(msg)); + }, +} satisfies Queue; diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc index 03fbcec65b..6d9c2b2324 100644 --- a/packages/gitbook-v2/wrangler.jsonc +++ b/packages/gitbook-v2/wrangler.jsonc @@ -15,10 +15,14 @@ "enabled": true }, "vars": { - "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true", + "IS_PREVIEW": "false" }, "env": { "preview": { + "vars": { + "IS_PREVIEW": "true" + }, "r2_buckets": [ { "binding": "NEXT_INC_CACHE_R2_BUCKET", From fae34e56ac40f6684beab5031cc1f00e8df35cbc Mon Sep 17 00:00:00 2001 From: Viktor Renkema <49148610+viktorrenkema@users.noreply.github.com> Date: Wed, 21 May 2025 14:20:39 +0200 Subject: [PATCH 055/127] Calculate class more explicitly & separately to fix horizontal table cell alignment issue (#3252) --- .../src/components/DocumentView/Table/RecordColumnValue.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index aa1bf45b86..50dc0d8270 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -115,7 +115,8 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>( return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>; } - const horizontalAlignment = `[&_*]:${getColumnAlignment(definition)} ${getColumnAlignment(definition)}`; + const horizontalAlignment = getColumnAlignment(definition); + const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`; return ( <Blocks @@ -131,6 +132,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>( 'leading-normal', verticalAlignment, horizontalAlignment, + childrenHorizontalAlignment, ]} context={context} blockStyle={['w-full', 'max-w-[unset]']} From 19c20c0be35374d7126db17009a40d313795ced8 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 21 May 2025 15:39:52 +0200 Subject: [PATCH 056/127] Fix OGImage icon fetch on cloudflare (#3253) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- packages/gitbook/src/routes/ogimage.tsx | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index c1cfd97723..ee586cb1b3 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -9,7 +9,6 @@ import { getAssetURL } from '@/lib/assets'; import { filterOutNullable } from '@/lib/typescript'; import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; -import { getCloudflareContext } from '@v2/lib/data/cloudflare'; import { getResizedImageURL } from '@v2/lib/images'; const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = { @@ -322,24 +321,6 @@ function logOnCloudflareOnly(message: string) { } } -/** - * Fetch a resource from the function itself. - * To avoid error with worker to worker requests in the same zone, we use the `WORKER_SELF_REFERENCE` binding. - */ -async function fetchSelf(url: string) { - const cloudflare = getCloudflareContext(); - if (cloudflare?.env.WORKER_SELF_REFERENCE) { - logOnCloudflareOnly(`Fetching self: ${url}`); - return await cloudflare.env.WORKER_SELF_REFERENCE.fetch( - // `getAssetURL` can return a relative URL, so we need to make it absolute - // the URL doesn't matter, as we're using the worker-self-reference binding - new URL(url, 'https://worker-self-reference/') - ); - } - - return await fetch(url); -} - /** * Read an image from a response as a base64 encoded string. */ @@ -363,6 +344,7 @@ const staticImagesCache = new Map<string, string>(); * Read a static image and cache it in memory. */ async function readStaticImage(url: string) { + logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticImagesCache.size}`); const cached = staticImagesCache.get(url); if (cached) { return cached; @@ -377,7 +359,7 @@ async function readStaticImage(url: string) { * Read an image from GitBook itself. */ async function readSelfImage(url: string) { - const response = await fetchSelf(url); + const response = await fetch(url); const image = await readImage(response); return image; } From a0c06a72cc8aa255bc8c068e6dca5a5701ff4314 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Fri, 23 May 2025 09:13:20 +0200 Subject: [PATCH 057/127] Indent JSON python code sample (#3260) --- .changeset/rotten-seals-rush.md | 5 ++++ .../react-openapi/src/code-samples.test.ts | 4 +-- packages/react-openapi/src/code-samples.ts | 28 +++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 .changeset/rotten-seals-rush.md diff --git a/.changeset/rotten-seals-rush.md b/.changeset/rotten-seals-rush.md new file mode 100644 index 0000000000..950e25880c --- /dev/null +++ b/.changeset/rotten-seals-rush.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Indent JSON python code sample diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts index 7fd24bb3f8..375cee84be 100644 --- a/packages/react-openapi/src/code-samples.test.ts +++ b/packages/react-openapi/src/code-samples.test.ts @@ -400,7 +400,7 @@ describe('python code sample generator', () => { const output = generator?.generate(input); expect(output).toBe( - 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={"key":"value"}\n)\n\ndata = response.json()' + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/x-www-form-urlencoded"},\n data={\n "key": "value"\n }\n)\n\ndata = response.json()' ); }); @@ -422,7 +422,7 @@ describe('python code sample generator', () => { const output = generator?.generate(input); expect(output).toBe( - 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()' + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({\n "key": "value",\n "truethy": True,\n "falsey": False,\n "nullish": None\n })\n)\n\ndata = response.json()' ); }); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 50c6a9204c..8d855bbb67 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -356,18 +356,22 @@ const BodyGenerators = { // Convert JSON to XML if needed body = JSON.stringify(convertBodyToXML(body)); } else { - body = stringifyOpenAPI(body, (_key, value) => { - switch (value) { - case true: - return '$$__TRUE__$$'; - case false: - return '$$__FALSE__$$'; - case null: - return '$$__NULL__$$'; - default: - return value; - } - }) + body = stringifyOpenAPI( + body, + (_key, value) => { + switch (value) { + case true: + return '$$__TRUE__$$'; + case false: + return '$$__FALSE__$$'; + case null: + return '$$__NULL__$$'; + default: + return value; + } + }, + 2 + ) .replaceAll('"$$__TRUE__$$"', 'True') .replaceAll('"$$__FALSE__$$"', 'False') .replaceAll('"$$__NULL__$$"', 'None'); From dc4268db64144c0e144d2541b317969a35fe5c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 23 May 2025 10:01:15 +0200 Subject: [PATCH 058/127] Compute correct urls for variants/sections when previewing unpublished site (#3259) --- .changeset/cool-seas-approve.md | 5 +++++ .../src/components/Header/SpacesDropdown.tsx | 14 +++++++++++++- .../SiteSections/encodeClientSiteSections.ts | 4 +++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/cool-seas-approve.md diff --git a/.changeset/cool-seas-approve.md b/.changeset/cool-seas-approve.md new file mode 100644 index 0000000000..c97f692e6d --- /dev/null +++ b/.changeset/cool-seas-approve.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix navigation between sections/variants when previewing a site in v2 diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 9728216b74..3fc1335230 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -2,6 +2,7 @@ import type { SiteSpace } from '@gitbook/api'; import { tcls } from '@/lib/tailwind'; +import { joinPath } from '@/lib/paths'; import type { GitBookSiteContext } from '@v2/lib/context'; import { DropdownChevron, DropdownMenu } from './DropdownMenu'; import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem'; @@ -74,7 +75,7 @@ export function SpacesDropdown(props: { title: otherSiteSpace.title, url: otherSiteSpace.urls.published ? linker.toLinkForContent(otherSiteSpace.urls.published) - : otherSiteSpace.space.urls.app, + : getFallbackSiteSpaceURL(otherSiteSpace, context), }} active={otherSiteSpace.id === siteSpace.id} /> @@ -82,3 +83,14 @@ export function SpacesDropdown(props: { </DropdownMenu> ); } + +/** + * When the site is not published yet, `urls.published` is not available. + * To ensure navigation works in preview, we compute a relative URL from the siteSpace path. + */ +function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) { + const { linker, sections } = context; + return linker.toPathInSite( + sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path + ); +} diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts index 79a007ecc3..49b73a8e1a 100644 --- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts +++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts @@ -53,6 +53,8 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) { description: section.description, icon: section.icon, object: section.object, - url: section.urls.published ? linker.toLinkForContent(section.urls.published) : '', + url: section.urls.published + ? linker.toLinkForContent(section.urls.published) + : linker.toPathInSite(section.path), }; } From fa3eb076177571914ab4bd40c83757bba64e0bc7 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Fri, 23 May 2025 10:20:07 +0200 Subject: [PATCH 059/127] Cache fonts and static image in memory for OG Image (#3258) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/stupid-plums-perform.md | 6 +++++ packages/gitbook/src/routes/ogimage.tsx | 34 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 .changeset/stupid-plums-perform.md diff --git a/.changeset/stupid-plums-perform.md b/.changeset/stupid-plums-perform.md new file mode 100644 index 0000000000..34707627ea --- /dev/null +++ b/.changeset/stupid-plums-perform.md @@ -0,0 +1,6 @@ +--- +"gitbook": patch +"gitbook-v2": patch +--- + +cache fonts and static image used in OGImage in memory diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index ee586cb1b3..b086ffd1cd 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -72,8 +72,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page const fonts = ( await Promise.all([ - loadGoogleFont({ fontFamily, text: regularText, weight: 400 }), - loadGoogleFont({ fontFamily, text: boldText, weight: 700 }), + getWithCache(`google-font:${fontFamily}:400`, () => + loadGoogleFont({ fontFamily, text: regularText, weight: 400 }) + ), + getWithCache(`google-font:${fontFamily}:700`, () => + loadGoogleFont({ fontFamily, text: boldText, weight: 700 }) + ), ]) ).filter(filterOutNullable); @@ -338,21 +342,27 @@ async function readImage(response: Response) { return `data:${contentType};base64,${base64}`; } -const staticImagesCache = new Map<string, string>(); +// biome-ignore lint/suspicious/noExplicitAny: <explanation> +const staticCache = new Map<string, any>(); + +// Do we need to limit the in-memory cache size? I think given the usage, we should be fine. +async function getWithCache<T>(key: string, fn: () => Promise<T>) { + const cached = staticCache.get(key) as T; + if (cached) { + return Promise.resolve(cached); + } + + const result = await fn(); + staticCache.set(key, result); + return result; +} /** * Read a static image and cache it in memory. */ async function readStaticImage(url: string) { - logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticImagesCache.size}`); - const cached = staticImagesCache.get(url); - if (cached) { - return cached; - } - - const image = await readSelfImage(url); - staticImagesCache.set(url, image); - return image; + logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`); + return getWithCache(`static-image:${url}`, () => readSelfImage(url)); } /** From 6f8ecd611138ce1a148687696a01009aa12452a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 23 May 2025 13:16:43 +0200 Subject: [PATCH 060/127] Use more strict context ID for images during preview (#3261) --- packages/gitbook-v2/src/lib/data/index.ts | 1 - packages/gitbook-v2/src/lib/data/visitor.ts | 2 +- .../images/getImageResizingContextId.test.ts | 19 +++++++++++++++++ .../lib/images/getImageResizingContextId.ts | 6 +++++- packages/gitbook-v2/src/lib/preview.test.ts | 21 +++++++++++++++++++ packages/gitbook-v2/src/lib/preview.ts | 13 ++++++++++++ .../src/lib/{data => }/proxy.test.ts | 0 .../gitbook-v2/src/lib/{data => }/proxy.ts | 0 8 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts create mode 100644 packages/gitbook-v2/src/lib/preview.test.ts create mode 100644 packages/gitbook-v2/src/lib/preview.ts rename packages/gitbook-v2/src/lib/{data => }/proxy.test.ts (100%) rename packages/gitbook-v2/src/lib/{data => }/proxy.ts (100%) diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts index d79049684a..2e37e2fbb4 100644 --- a/packages/gitbook-v2/src/lib/data/index.ts +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -4,5 +4,4 @@ export * from './pages'; export * from './urls'; export * from './errors'; export * from './lookup'; -export * from './proxy'; export * from './visitor'; diff --git a/packages/gitbook-v2/src/lib/data/visitor.ts b/packages/gitbook-v2/src/lib/data/visitor.ts index f93f0afe87..e59e32365c 100644 --- a/packages/gitbook-v2/src/lib/data/visitor.ts +++ b/packages/gitbook-v2/src/lib/data/visitor.ts @@ -1,6 +1,6 @@ import { withLeadingSlash, withTrailingSlash } from '@/lib/paths'; import type { PublishedSiteContent } from '@gitbook/api'; -import { getProxyRequestIdentifier, isProxyRequest } from './proxy'; +import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy'; /** * Get the appropriate base path for the visitor authentication cookie. diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts new file mode 100644 index 0000000000..eaba01663b --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'bun:test'; +import { getImageResizingContextId } from './getImageResizingContextId'; + +describe('getImageResizingContextId', () => { + it('should return proxy identifier for proxy requests', () => { + const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'); + expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo'); + }); + + it('should return preview identifier for preview requests', () => { + const previewRequestURL = new URL('https://preview/site_foo/hello/world'); + expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo'); + }); + + it('should return host for regular requests', () => { + const regularRequestURL = new URL('https://example.com/docs/foo/hello/world'); + expect(getImageResizingContextId(regularRequestURL)).toBe('example.com'); + }); +}); diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts index 82f9225262..40594e2ae2 100644 --- a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts +++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts @@ -1,4 +1,5 @@ -import { getProxyRequestIdentifier, isProxyRequest } from '../data'; +import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview'; +import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy'; /** * Get the site identifier to use for image resizing for an incoming request. @@ -8,6 +9,9 @@ export function getImageResizingContextId(url: URL): string { if (isProxyRequest(url)) { return getProxyRequestIdentifier(url); } + if (isPreviewRequest(url)) { + return getPreviewRequestIdentifier(url); + } return url.host; } diff --git a/packages/gitbook-v2/src/lib/preview.test.ts b/packages/gitbook-v2/src/lib/preview.test.ts new file mode 100644 index 0000000000..bbaf0402bd --- /dev/null +++ b/packages/gitbook-v2/src/lib/preview.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'bun:test'; +import { getPreviewRequestIdentifier, isPreviewRequest } from './preview'; + +describe('isPreviewRequest', () => { + it('should return true for preview requests', () => { + const previewRequestURL = new URL('https://preview/site_foo/hello/world'); + expect(isPreviewRequest(previewRequestURL)).toBe(true); + }); + + it('should return false for non-preview requests', () => { + const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world'); + expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false); + }); +}); + +describe('getPreviewRequestIdentifier', () => { + it('should return the correct identifier for preview requests', () => { + const previewRequestURL = new URL('https://preview/site_foo/hello/world'); + expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo'); + }); +}); diff --git a/packages/gitbook-v2/src/lib/preview.ts b/packages/gitbook-v2/src/lib/preview.ts new file mode 100644 index 0000000000..7094d11970 --- /dev/null +++ b/packages/gitbook-v2/src/lib/preview.ts @@ -0,0 +1,13 @@ +/** + * Check if the request to the site is a preview request. + */ +export function isPreviewRequest(requestURL: URL): boolean { + return requestURL.host === 'preview'; +} + +export function getPreviewRequestIdentifier(requestURL: URL): string { + // For preview requests, we extract the site ID from the pathname + // e.g. https://preview/site_id/... + const pathname = requestURL.pathname.slice(1).split('/'); + return pathname[0]; +} diff --git a/packages/gitbook-v2/src/lib/data/proxy.test.ts b/packages/gitbook-v2/src/lib/proxy.test.ts similarity index 100% rename from packages/gitbook-v2/src/lib/data/proxy.test.ts rename to packages/gitbook-v2/src/lib/proxy.test.ts diff --git a/packages/gitbook-v2/src/lib/data/proxy.ts b/packages/gitbook-v2/src/lib/proxy.ts similarity index 100% rename from packages/gitbook-v2/src/lib/data/proxy.ts rename to packages/gitbook-v2/src/lib/proxy.ts From af66ff71ed65b0e215377fcfa9424d9837c9e661 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Sun, 25 May 2025 16:55:45 +0200 Subject: [PATCH 061/127] Add a force-revalidate api route (#3263) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/gorgeous-cycles-cheat.md | 5 ++ packages/gitbook-v2/next-env.d.ts | 1 + .../pages/api/~gitbook/force-revalidate.ts | 49 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 .changeset/gorgeous-cycles-cheat.md create mode 100644 packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts diff --git a/.changeset/gorgeous-cycles-cheat.md b/.changeset/gorgeous-cycles-cheat.md new file mode 100644 index 0000000000..d13c27765a --- /dev/null +++ b/.changeset/gorgeous-cycles-cheat.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +add a force-revalidate api route to force bust the cache in case of errors diff --git a/packages/gitbook-v2/next-env.d.ts b/packages/gitbook-v2/next-env.d.ts index 1b3be0840f..3cd7048ed9 100644 --- a/packages/gitbook-v2/next-env.d.ts +++ b/packages/gitbook-v2/next-env.d.ts @@ -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/app/api-reference/config/typescript for more information. diff --git a/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts new file mode 100644 index 0000000000..45e6c7eca1 --- /dev/null +++ b/packages/gitbook-v2/src/pages/api/~gitbook/force-revalidate.ts @@ -0,0 +1,49 @@ +import crypto from 'node:crypto'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +interface JsonBody { + // The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware + paths: string[]; +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Only allow POST requests + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined; + if (!signatureHeader) { + return res.status(400).json({ error: 'Missing signature header' }); + } + // We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file. + if (process.env.GITBOOK_SECRET) { + try { + const computedSignature = crypto + .createHmac('sha256', process.env.GITBOOK_SECRET) + .update(JSON.stringify(req.body)) + .digest('hex'); + + if (computedSignature === signatureHeader) { + const results = await Promise.allSettled( + (req.body as JsonBody).paths.map((path) => { + // biome-ignore lint/suspicious/noConsole: we want to log here + console.log(`Revalidating path: ${path}`); + return res.revalidate(path); + }) + ); + return res.status(200).json({ + success: results.every((result) => result.status === 'fulfilled'), + errors: results + .filter((result) => result.status === 'rejected') + .map((result) => (result as PromiseRejectedResult).reason), + }); + } + return res.status(401).json({ error: 'Invalid signature' }); + } catch (error) { + console.error('Error during revalidation:', error); + return res.status(400).json({ error: 'Invalid request or unable to parse JSON' }); + } + } + // If no secret is set, we do not allow revalidation + return res.status(403).json({ error: 'Revalidation is disabled' }); +} From 40df91a3639e8bfd93c589d5fb3c8ff552c5232e Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Mon, 26 May 2025 18:55:11 +0200 Subject: [PATCH 062/127] Deduplicate path parameters from OpenAPI spec (#3264) --- .changeset/violet-schools-care.md | 5 +++++ packages/react-openapi/src/OpenAPISpec.tsx | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/violet-schools-care.md diff --git a/.changeset/violet-schools-care.md b/.changeset/violet-schools-care.md new file mode 100644 index 0000000000..e77a98b5cf --- /dev/null +++ b/.changeset/violet-schools-care.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Deduplicate path parameters from OpenAPI spec diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index 49c41cda40..1e6e562690 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -18,7 +18,7 @@ export function OpenAPISpec(props: { const { operation } = data; - const parameters = operation.parameters ?? []; + const parameters = deduplicateParameters(operation.parameters ?? []); const parameterGroups = groupParameters(parameters, context); const securities = 'securities' in data ? data.securities : []; @@ -113,3 +113,23 @@ function getParameterGroupName(paramIn: string, context: OpenAPIClientContext): return paramIn; } } + +/** Deduplicate parameters by name and in. + * Some specs have both parameters define at path and operation level. + * We only want to display one of them. + */ +function deduplicateParameters(parameters: OpenAPI.Parameters): OpenAPI.Parameters { + const seen = new Set(); + + return parameters.filter((param) => { + const key = `${param.name}:${param.in}`; + + if (seen.has(key)) { + return false; + } + + seen.add(key); + + return true; + }); +} From 4c9a9d0416c91398198027f637b1aa7049f20899 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Mon, 26 May 2025 20:28:26 +0200 Subject: [PATCH 063/127] Handle nested deprecated properties in generateSchemaExample (#3267) --- .changeset/thick-chefs-repeat.md | 5 +++++ .../src/generateSchemaExample.test.ts | 20 +++++++++++++++++++ .../src/generateSchemaExample.ts | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/thick-chefs-repeat.md diff --git a/.changeset/thick-chefs-repeat.md b/.changeset/thick-chefs-repeat.md new file mode 100644 index 0000000000..8b08d50e3e --- /dev/null +++ b/.changeset/thick-chefs-repeat.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Handle nested deprecated properties in generateSchemaExample diff --git a/packages/react-openapi/src/generateSchemaExample.test.ts b/packages/react-openapi/src/generateSchemaExample.test.ts index 3181881b62..682c5f2f38 100644 --- a/packages/react-openapi/src/generateSchemaExample.test.ts +++ b/packages/react-openapi/src/generateSchemaExample.test.ts @@ -1017,4 +1017,24 @@ describe('generateSchemaExample', () => { }, }); }); + + it('handles deprecated properties', () => { + expect( + generateSchemaExample({ + type: 'object', + deprecated: true, + }) + ).toBeUndefined(); + }); + + it('handle nested deprecated properties', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + deprecated: true, + }, + }) + ).toBeUndefined(); + }); }); diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index 5a038db06b..595f723d91 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -167,7 +167,7 @@ const getExampleFromSchema = ( const makeUpRandomData = !!options?.emptyString; // If the property is deprecated we don't show it in examples. - if (schema.deprecated) { + if (schema.deprecated || (schema.type === 'array' && schema.items?.deprecated)) { return undefined; } From 957afd967fc306c404046d27c240bed34bdd2115 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Mon, 26 May 2025 20:54:07 +0200 Subject: [PATCH 064/127] Add authorization header for OAuth2 (#3266) --- .changeset/fast-trees-battle.md | 5 +++++ packages/react-openapi/src/OpenAPICodeSample.tsx | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/fast-trees-battle.md diff --git a/.changeset/fast-trees-battle.md b/.changeset/fast-trees-battle.md new file mode 100644 index 0000000000..0e75fbbeda --- /dev/null +++ b/.changeset/fast-trees-battle.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': patch +--- + +Add authorization header for OAuth2 diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 8b67bbedc4..8bffb6d9ad 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -312,6 +312,11 @@ function getSecurityHeaders(securities: OpenAPIOperationData['securities']): { [name]: 'YOUR_API_KEY', }; } + case 'oauth2': { + return { + Authorization: 'Bearer YOUR_OAUTH2_TOKEN', + }; + } default: { return {}; } From 2932077bf90ca92eadbfd8b9c832c5fd6f018e0b Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Tue, 27 May 2025 21:13:55 +0200 Subject: [PATCH 065/127] Remove trailing slash from linker (#3268) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/wise-gifts-smash.md | 5 +++++ packages/gitbook-v2/src/lib/links.test.ts | 22 +++++++++++++++++++++- packages/gitbook-v2/src/lib/links.ts | 6 +++++- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/wise-gifts-smash.md diff --git a/.changeset/wise-gifts-smash.md b/.changeset/wise-gifts-smash.md new file mode 100644 index 0000000000..32a85eecb7 --- /dev/null +++ b/.changeset/wise-gifts-smash.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +remove trailing slash from linker diff --git a/packages/gitbook-v2/src/lib/links.test.ts b/packages/gitbook-v2/src/lib/links.test.ts index 26a2464f50..f73ec5ee86 100644 --- a/packages/gitbook-v2/src/lib/links.test.ts +++ b/packages/gitbook-v2/src/lib/links.test.ts @@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({ siteBasePath: '/sitename/', }); -describe('toPathInContent', () => { +describe('toPathInSpace', () => { it('should return the correct path', () => { expect(root.toPathInSpace('some/path')).toBe('/some/path'); expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path'); @@ -29,6 +29,16 @@ describe('toPathInContent', () => { expect(root.toPathInSpace('/some/path')).toBe('/some/path'); expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path'); }); + + it('should remove the trailing slash', () => { + expect(root.toPathInSpace('some/path/')).toBe('/some/path'); + expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path'); + }); + + it('should not add a trailing slash', () => { + expect(root.toPathInSpace('')).toBe(''); + expect(variantInSection.toPathInSpace('')).toBe('/section/variant'); + }); }); describe('toPathInSite', () => { @@ -36,6 +46,16 @@ describe('toPathInSite', () => { expect(root.toPathInSite('some/path')).toBe('/some/path'); expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path'); }); + + it('should remove the trailing slash', () => { + expect(root.toPathInSite('some/path/')).toBe('/some/path'); + expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path'); + }); + + it('should not add a trailing slash', () => { + expect(root.toPathInSite('')).toBe(''); + expect(siteGitBookIO.toPathInSite('')).toBe('/sitename'); + }); }); describe('toRelativePathInSite', () => { diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts index b84565d1e7..a64bda541c 100644 --- a/packages/gitbook-v2/src/lib/links.ts +++ b/packages/gitbook-v2/src/lib/links.ts @@ -128,5 +128,9 @@ export function createLinker( function joinPaths(prefix: string, path: string): string { const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`; const suffixPath = path.startsWith('/') ? path.slice(1) : path; - return prefixPath + suffixPath; + return removeTrailingSlash(prefixPath + suffixPath); +} + +function removeTrailingSlash(path: string): string { + return path.endsWith('/') ? path.slice(0, -1) : path; } From b3a7ad6d2cfc1a55fd3c15c0baefcd39bd3228cf Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 28 May 2025 17:07:33 +0200 Subject: [PATCH 066/127] Fix href being empty in TOC (#3269) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/cool-jars-matter.md | 5 +++++ .../src/components/TableOfContents/PageDocumentItem.tsx | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/cool-jars-matter.md diff --git a/.changeset/cool-jars-matter.md b/.changeset/cool-jars-matter.md new file mode 100644 index 0000000000..ff0c934f33 --- /dev/null +++ b/.changeset/cool-jars-matter.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +fix href being empty in TOC diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx index 2ff3289d7f..4875633ac9 100644 --- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx @@ -17,7 +17,11 @@ export async function PageDocumentItem(props: { context: GitBookSiteContext; }) { const { rootPages, page, context } = props; - const href = context.linker.toPathForPage({ pages: rootPages, page }); + let href = context.linker.toPathForPage({ pages: rootPages, page }); + // toPathForPage can returns an empty path, this will cause all links to point to the current page. + if (href === '') { + href = '/'; + } return ( <li className="flex flex-col"> From df2fa42bda09a85a3ca2412ed898e6e26cf77586 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Fri, 30 May 2025 14:31:21 +0200 Subject: [PATCH 067/127] Chores: create server worker (#3265) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../composite/deploy-cloudflare/action.yaml | 11 ++++++ .../openNext/customWorkers/default.js | 9 +++++ .../customWorkers/defaultWrangler.jsonc | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 packages/gitbook-v2/openNext/customWorkers/default.js create mode 100644 packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index fbc98fc82f..1cd19c4d5d 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -73,6 +73,17 @@ runs: wranglerVersion: '4.10.0' environment: ${{ inputs.environment }} command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc + + - name: Temporary deploy server CF worker + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: 'deploy --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc' + - name: Outputs shell: bash env: diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js new file mode 100644 index 0000000000..6e2f1af82a --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/default.js @@ -0,0 +1,9 @@ +export default { + async fetch() { + return new Response('Hello World', { + headers: { + 'Content-Type': 'text/plain', + }, + }); + }, +}; diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc new file mode 100644 index 0000000000..5ade8d025f --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc @@ -0,0 +1,38 @@ +{ + "main": "default.js", + "name": "gitbook-open-v2-server", + "compatibility_date": "2025-04-14", + "compatibility_flags": [ + "nodejs_compat", + "allow_importable_env", + "global_fetch_strictly_public" + ], + "observability": { + "enabled": true + }, + "vars": { + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + }, + "env": { + "dev": { + "vars": { + "STAGE": "dev" + } + }, + "preview": { + "vars": { + "STAGE": "preview" + } + }, + "staging": { + "vars": { + "STAGE": "staging" + } + }, + "production": { + "vars": { + "STAGE": "production" + } + } + } +} From 666e8426e1a39fffdb4de586bad633a1a2049365 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Fri, 30 May 2025 14:56:00 +0200 Subject: [PATCH 068/127] External middleware in cloudflare (#3254) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../gradual-deploy-cloudflare/action.yaml | 83 ++++++++ .../composite/deploy-cloudflare/action.yaml | 54 ++++- packages/gitbook-v2/open-next.config.ts | 53 ++--- .../openNext/customWorkers/default.js | 16 +- .../customWorkers/defaultWrangler.jsonc | 96 ++++++++- .../openNext/customWorkers/middleware.js | 42 ++++ .../customWorkers/middlewareWrangler.jsonc | 185 ++++++++++++++++++ .../customWorkers/script/updateWrangler.ts | 26 +++ packages/gitbook-v2/openNext/queue.ts | 17 -- .../gitbook-v2/openNext/queue/middleware.ts | 21 ++ packages/gitbook-v2/openNext/queue/server.ts | 9 + .../openNext/tagCache/middleware.ts | 76 +++++++ packages/gitbook-v2/package.json | 2 + packages/gitbook-v2/src/lib/data/api.ts | 23 +-- 14 files changed, 616 insertions(+), 87 deletions(-) create mode 100644 .github/actions/gradual-deploy-cloudflare/action.yaml create mode 100644 packages/gitbook-v2/openNext/customWorkers/middleware.js create mode 100644 packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc create mode 100644 packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts delete mode 100644 packages/gitbook-v2/openNext/queue.ts create mode 100644 packages/gitbook-v2/openNext/queue/middleware.ts create mode 100644 packages/gitbook-v2/openNext/queue/server.ts create mode 100644 packages/gitbook-v2/openNext/tagCache/middleware.ts diff --git a/.github/actions/gradual-deploy-cloudflare/action.yaml b/.github/actions/gradual-deploy-cloudflare/action.yaml new file mode 100644 index 0000000000..eff064a754 --- /dev/null +++ b/.github/actions/gradual-deploy-cloudflare/action.yaml @@ -0,0 +1,83 @@ +name: Gradual Deploy to Cloudflare +description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together +inputs: + apiToken: + description: 'Cloudflare API token' + required: true + accountId: + description: 'Cloudflare account ID' + required: true + environment: + description: 'Cloudflare environment to deploy to (staging, production, preview)' + required: true + middlewareVersionId: + description: 'Middleware version ID to deploy' + required: true + serverVersionId: + description: 'Server version ID to deploy' + required: true +outputs: + deployment-url: + description: "Deployment URL" + value: ${{ steps.deploy_middleware.outputs.deployment-url }} +runs: + using: 'composite' + steps: + - id: wrangler_status + name: Check wrangler deployment status + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc + + # This step is used to get the version ID that is currently deployed to Cloudflare. + - id: extract_current_version + name: Extract current version + shell: bash + run: | + version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}') + echo "version_id=$version_id" >> $GITHUB_OUTPUT + + - id: deploy_server + name: Deploy server to Cloudflare at 0% + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc + + # Since we use version overrides headers, we can directly deploy the middleware to 100%. + - id: deploy_middleware + name: Deploy middleware to Cloudflare at 100% + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc + + - name: Deploy server to Cloudflare at 100% + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc + + - name: Outputs + shell: bash + env: + DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }} + run: | + echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}" \ No newline at end of file diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index 1cd19c4d5d..d1d0d66339 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -28,7 +28,7 @@ inputs: outputs: deployment-url: description: "Deployment URL" - value: ${{ steps.deploy.outputs.deployment-url }} + value: ${{ steps.upload_middleware.outputs.deployment-url }} runs: using: 'composite' steps: @@ -63,8 +63,9 @@ runs: env: GITBOOK_RUNTIME: cloudflare shell: bash - - id: deploy - name: Deploy to Cloudflare + + - id: upload_server + name: Upload server to Cloudflare uses: cloudflare/wrangler-action@v3.14.0 with: apiToken: ${{ inputs.apiToken }} @@ -72,9 +73,22 @@ runs: workingDirectory: ./ wranglerVersion: '4.10.0' environment: ${{ inputs.environment }} - command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc - - - name: Temporary deploy server CF worker + command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc + + - name: Extract server version worker ID + shell: bash + id: extract_server_version_id + run: | + version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}') + echo "version_id=$version_id" >> $GITHUB_OUTPUT + + - name: Run updateWrangler scripts + shell: bash + run: | + bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }} + + - id: upload_middleware + name: Upload middleware to Cloudflare uses: cloudflare/wrangler-action@v3.14.0 with: apiToken: ${{ inputs.apiToken }} @@ -82,11 +96,33 @@ runs: workingDirectory: ./ wranglerVersion: '4.10.0' environment: ${{ inputs.environment }} - command: 'deploy --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc' + command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc + - name: Extract middleware version worker ID + shell: bash + id: extract_middleware_version_id + run: | + version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}') + echo "version_id=$version_id" >> $GITHUB_OUTPUT + + - name: Deploy server and middleware to Cloudflare + if: ${{ inputs.deploy == 'true' }} + uses: ./.github/actions/gradual-deploy-cloudflare + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + opServiceAccount: ${{ inputs.opServiceAccount }} + opItem: ${{ inputs.opItem }} + environment: ${{ inputs.environment }} + serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }} + middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }} + deploy: ${{ inputs.deploy }} + + - name: Outputs shell: bash env: - DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }} run: | - echo "URL: ${{ steps.deploy.outputs.deployment-url }}" \ No newline at end of file + echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}" + echo "Output server: ${{ steps.upload_server.outputs.command-output }}" \ No newline at end of file diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index d35c9ef03e..959834739a 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -1,26 +1,29 @@ -import { defineCloudflareConfig } from '@opennextjs/cloudflare'; -import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; -import { - softTagFilter, - withFilter, -} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; +import type { OpenNextConfig } from '@opennextjs/cloudflare'; -export default defineCloudflareConfig({ - incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), - tagCache: withFilter({ - tagCache: doShardedTagCache({ - baseShardSize: 12, - regionalCache: true, - shardReplication: { - numberOfSoftReplicas: 2, - numberOfHardReplicas: 1, - }, - }), - // We don't use `revalidatePath`, so we filter out soft tags - filterFn: softTagFilter, - }), - queue: () => import('./openNext/queue').then((m) => m.default), - - // Performance improvements as we don't use PPR - enableCacheInterception: true, -}); +export default { + default: { + override: { + wrapper: 'cloudflare-node', + converter: 'edge', + proxyExternalRequest: 'fetch', + queue: () => import('./openNext/queue/server').then((m) => m.default), + incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), + tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default), + }, + }, + middleware: { + external: true, + override: { + wrapper: 'cloudflare-edge', + converter: 'edge', + proxyExternalRequest: 'fetch', + queue: () => import('./openNext/queue/middleware').then((m) => m.default), + incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), + tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default), + }, + }, + dangerous: { + enableCacheInterception: true, + }, + edgeExternals: ['node:crypto'], +} satisfies OpenNextConfig; diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js index 6e2f1af82a..bb673e1722 100644 --- a/packages/gitbook-v2/openNext/customWorkers/default.js +++ b/packages/gitbook-v2/openNext/customWorkers/default.js @@ -1,9 +1,15 @@ +import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js'; + export default { - async fetch() { - return new Response('Hello World', { - headers: { - 'Content-Type': 'text/plain', - }, + async fetch(request, env, ctx) { + return runWithCloudflareRequestContext(request, env, ctx, async () => { + // We can't move the handler import to the top level, otherwise the runtime will not be properly initialized + const { handler } = await import( + '../../.open-next/server-functions/default/handler.mjs' + ); + + // - `Request`s are handled by the Next server + return handler(request, env, ctx); }); }, }; diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc index 5ade8d025f..3fff07d7c0 100644 --- a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc +++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc @@ -17,22 +17,100 @@ "dev": { "vars": { "STAGE": "dev" - } + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-server-dev" + } + ] }, "preview": { "vars": { - "STAGE": "preview" - } + "STAGE": "preview", + // Just as a test for the preview environment to check that everything works + "NEXT_PRIVATE_DEBUG_CACHE": "true" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-server-preview" + } + ] + // No durable objects on preview, as they block the generation of preview URLs + // and we don't need tags invalidation on preview }, "staging": { - "vars": { - "STAGE": "staging" - } + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-staging" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-server-staging" + } + ], + "durable_objects": { + "bindings": [ + // We do not need to define migrations for external DOs, + // In fact, defining migrations for external DOs will crash + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-staging" + } + ] + }, + "tail_consumers": [ + { + "service": "gitbook-x-staging-tail" + } + ] }, "production": { - "vars": { - "STAGE": "production" - } + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-production" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-server-production" + } + ], + "durable_objects": { + "bindings": [ + { + // We do not need to define migrations for external DOs, + // In fact, defining migrations for external DOs will crash + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-production" + } + ] + }, + "tail_consumers": [ + { + "service": "gitbook-x-prod-tail" + } + ] } } } diff --git a/packages/gitbook-v2/openNext/customWorkers/middleware.js b/packages/gitbook-v2/openNext/customWorkers/middleware.js new file mode 100644 index 0000000000..78a84a9760 --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/middleware.js @@ -0,0 +1,42 @@ +import { WorkerEntrypoint } from 'cloudflare:workers'; +import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js'; + +import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs'; + +export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js'; + +export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js'; + +export default class extends WorkerEntrypoint { + async fetch(request) { + return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => { + // - `Request`s are handled by the Next server + const reqOrResp = await middlewareHandler(request, this.env, this.ctx); + if (reqOrResp instanceof Response) { + return reqOrResp; + } + + if (this.env.STAGE !== 'preview') { + // https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity + reqOrResp.headers.set( + 'Cloudflare-Workers-Version-Overrides', + `gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"` + ); + return this.env.DEFAULT_WORKER?.fetch(reqOrResp, { + cf: { + cacheEverything: false, + }, + }); + } + // If we are in preview mode, we need to send the request to the preview URL + const modifiedUrl = new URL(reqOrResp.url); + modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME; + const nextRequest = new Request(modifiedUrl, reqOrResp); + return fetch(nextRequest, { + cf: { + cacheEverything: false, + }, + }); + }); + } +} diff --git a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc new file mode 100644 index 0000000000..4870c89d54 --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc @@ -0,0 +1,185 @@ +{ + "main": "middleware.js", + "name": "gitbook-open-v2", + "compatibility_date": "2025-04-14", + "compatibility_flags": [ + "nodejs_compat", + "allow_importable_env", + "global_fetch_strictly_public" + ], + "assets": { + "directory": "../../.open-next/assets", + "binding": "ASSETS" + }, + "observability": { + "enabled": true + }, + "vars": { + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + }, + "env": { + "dev": { + "vars": { + "STAGE": "dev", + "NEXT_PRIVATE_DEBUG_CACHE": "true" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-dev" + }, + { + "binding": "DEFAULT_WORKER", + "service": "gitbook-open-v2-server-dev" + } + ] + }, + "preview": { + "vars": { + "STAGE": "preview", + "PREVIEW_HOSTNAME": "TO_REPLACE", + "WORKER_VERSION_ID": "TO_REPLACE" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-preview" + }, + { + "binding": "DEFAULT_WORKER", + "service": "gitbook-open-v2-server-preview" + } + ] + // No durable objects on preview, as they block the generation of preview URLs + // and we don't need tags invalidation on preview + }, + "staging": { + "vars": { + "STAGE": "staging", + "WORKER_VERSION_ID": "TO_REPLACE" + }, + "routes": [ + { + "pattern": "open-2c.gitbook-staging.com/*", + "zone_name": "gitbook-staging.com" + }, + { + "pattern": "static-2c.gitbook-staging.com/*", + "zone_name": "gitbook-staging.com" + } + ], + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-staging" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-staging" + }, + { + "binding": "DEFAULT_WORKER", + "service": "gitbook-open-v2-server-staging" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-staging-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"] + } + ] + }, + "production": { + "vars": { + // This is a bit misleading, but it means that we can have 500 concurrent revalidations + // This means that we'll have up to 100 durable objects instance running at the same time + "MAX_REVALIDATE_CONCURRENCY": "100", + // Temporary variable to find the issue once deployed + // TODO: remove this once the issue is fixed + "DEBUG_CLOUDFLARE": "true", + "WORKER_VERSION_ID": "TO_REPLACE", + "STAGE": "production" + }, + "routes": [ + { + "pattern": "open-2c.gitbook.com/*", + "zone_name": "gitbook.com" + }, + { + "pattern": "static-2c.gitbook.com/*", + "zone_name": "gitbook.com" + } + ], + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-production" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-production" + }, + { + "binding": "DEFAULT_WORKER", + "service": "gitbook-open-v2-server-production" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-prod-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"] + } + ] + } + } +} diff --git a/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts new file mode 100644 index 0000000000..0fdbf6cc70 --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts @@ -0,0 +1,26 @@ +// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware +import fs from 'node:fs'; +import path from 'node:path'; + +const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc'); + +const file = fs.readFileSync(wranglerConfigPath, 'utf-8'); + +const args = process.argv.slice(2); +// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL +const versionId = args[0]; + +// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev +const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`; + +let updatedFile = file.replace( + /"PREVIEW_HOSTNAME": "TO_REPLACE"/, + `"PREVIEW_HOSTNAME": "${previewHostname}"` +); + +updatedFile = updatedFile.replaceAll( + /"WORKER_VERSION_ID": "TO_REPLACE"/g, + `"WORKER_VERSION_ID": "${versionId}"` +); + +fs.writeFileSync(wranglerConfigPath, updatedFile); diff --git a/packages/gitbook-v2/openNext/queue.ts b/packages/gitbook-v2/openNext/queue.ts deleted file mode 100644 index ab33c479d9..0000000000 --- a/packages/gitbook-v2/openNext/queue.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Queue } from '@opennextjs/aws/types/overrides.js'; -import { getCloudflareContext } from '@opennextjs/cloudflare'; -import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; -import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue'; - -interface Env { - IS_PREVIEW?: string; -} - -export default { - name: 'GitbookISRQueue', - send: async (msg) => { - const { ctx, env } = getCloudflareContext(); - const isPreview = (env as Env).IS_PREVIEW === 'true'; - ctx.waitUntil(isPreview ? memoryQueue.send(msg) : doQueue.send(msg)); - }, -} satisfies Queue; diff --git a/packages/gitbook-v2/openNext/queue/middleware.ts b/packages/gitbook-v2/openNext/queue/middleware.ts new file mode 100644 index 0000000000..2a14dc1d8b --- /dev/null +++ b/packages/gitbook-v2/openNext/queue/middleware.ts @@ -0,0 +1,21 @@ +import { trace } from '@/lib/tracing'; +import type { Queue } from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; +import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; +import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue'; + +interface Env { + STAGE?: string; +} + +export default { + name: 'GitbookISRQueue', + send: async (msg) => { + return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => { + const { ctx, env } = getCloudflareContext(); + const hasDurableObject = + (env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview'; + ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg)); + }); + }, +} satisfies Queue; diff --git a/packages/gitbook-v2/openNext/queue/server.ts b/packages/gitbook-v2/openNext/queue/server.ts new file mode 100644 index 0000000000..9a5b3b689b --- /dev/null +++ b/packages/gitbook-v2/openNext/queue/server.ts @@ -0,0 +1,9 @@ +import type { Queue } from '@opennextjs/aws/types/overrides.js'; + +export default { + name: 'GitbookISRQueue', + send: async (msg) => { + // We should never reach this point in the server. If that's the case, we should log it. + console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg); + }, +} satisfies Queue; diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts new file mode 100644 index 0000000000..173b5dd283 --- /dev/null +++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts @@ -0,0 +1,76 @@ +import { trace } from '@/lib/tracing'; +import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js'; +import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; +import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; + +const originalTagCache = doShardedTagCache({ + baseShardSize: 12, + regionalCache: true, + shardReplication: { + numberOfSoftReplicas: 2, + numberOfHardReplicas: 1, + }, +}); + +export default { + name: 'GitbookTagCache', + mode: 'nextMode', + getLastRevalidated: async (tags: string[]) => { + const tagsToCheck = tags.filter(softTagFilter); + if (tagsToCheck.length === 0) { + // If we reach here, it probably means that there is an issue that we'll need to address. + console.warn( + 'getLastRevalidated - No valid tags to check for last revalidation, original tags:', + tags + ); + return 0; // If no tags to check, return 0 + } + return trace( + { + operation: 'gitbookTagCacheGetLastRevalidated', + name: tagsToCheck.join(', '), + }, + async () => { + return await originalTagCache.getLastRevalidated(tagsToCheck); + } + ); + }, + hasBeenRevalidated: async (tags: string[]) => { + const tagsToCheck = tags.filter(softTagFilter); + if (tagsToCheck.length === 0) { + // If we reach here, it probably means that there is an issue that we'll need to address. + console.warn( + 'hasBeenRevalidated - No valid tags to check for revalidation, original tags:', + tags + ); + return false; // If no tags to check, return false + } + return trace( + { + operation: 'gitbookTagCacheHasBeenRevalidated', + name: tagsToCheck.join(', '), + }, + async () => { + const result = await originalTagCache.hasBeenRevalidated(tagsToCheck); + return result; + } + ); + }, + writeTags: async (tags: string[]) => { + return trace( + { + operation: 'gitbookTagCacheWriteTags', + name: tags.join(', '), + }, + async () => { + const tagsToWrite = tags.filter(softTagFilter); + if (tagsToWrite.length === 0) { + console.warn('writeTags - No valid tags to write'); + return; // If no tags to write, exit early + } + // Write only the filtered tags + await originalTagCache.writeTags(tagsToWrite); + } + ); + }, +} satisfies NextModeTagCache; diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 5561b7f8d3..224d55e301 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -30,6 +30,8 @@ "start": "next start", "build:v2:cloudflare": "opennextjs-cloudflare build", "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview", + "dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc", + "dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc", "unit": "bun test", "typecheck": "tsc --noEmit" } diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 7a2d03d16d..f8ae6d2711 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -2,14 +2,13 @@ import { trace } from '@/lib/tracing'; import { type ComputedContentSource, GitBookAPI, - type GitBookAPIServiceBinding, type HttpResponse, type RenderIntegrationUI, } from '@gitbook/api'; import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; -import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare'; +import { getCloudflareRequestGlobal } from './cloudflare'; import { DataFetcherError, wrapDataFetcherError } from './errors'; import { withCacheKey, withoutConcurrentExecution } from './memoize'; import type { GitBookDataFetcher } from './types'; @@ -828,36 +827,16 @@ async function* streamAIResponse( } } -let loggedServiceBinding = false; - /** * Create a new API client. */ export function apiClient(input: DataFetcherInput = { apiToken: null }) { const { apiToken } = input; - let serviceBinding: GitBookAPIServiceBinding | undefined; - - const cloudflareContext = getCloudflareContext(); - if (cloudflareContext) { - // @ts-expect-error - serviceBinding = cloudflareContext.env.GITBOOK_API as GitBookAPIServiceBinding | undefined; - if (!loggedServiceBinding) { - loggedServiceBinding = true; - if (serviceBinding) { - // biome-ignore lint/suspicious/noConsole: we want to log here - console.log(`using service binding for the API (${GITBOOK_API_URL})`); - } else { - // biome-ignore lint/suspicious/noConsole: we want to log here - console.warn(`no service binding for the API (${GITBOOK_API_URL})`); - } - } - } const api = new GitBookAPI({ authToken: apiToken || GITBOOK_API_TOKEN || undefined, endpoint: GITBOOK_API_URL, userAgent: GITBOOK_USER_AGENT, - serviceBinding, }); return api; From a1da4f023d7d491dde259f526c66d922d5015645 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Fri, 30 May 2025 16:19:11 +0200 Subject: [PATCH 069/127] Move DO to a 3rd worker (#3270) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../composite/deploy-cloudflare/action.yaml | 10 ++ .../openNext/customWorkers/default.js | 21 +++ .../customWorkers/defaultWrangler.jsonc | 65 +++++++-- .../gitbook-v2/openNext/customWorkers/do.js | 38 ++++++ .../openNext/customWorkers/doWrangler.jsonc | 127 ++++++++++++++++++ .../customWorkers/middlewareWrangler.jsonc | 49 +++++-- .../gitbook-v2/openNext/incrementalCache.ts | 22 ++- .../openNext/tagCache/middleware.ts | 6 +- 8 files changed, 315 insertions(+), 23 deletions(-) create mode 100644 packages/gitbook-v2/openNext/customWorkers/do.js create mode 100644 packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index d1d0d66339..7e66a8bf33 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -64,6 +64,16 @@ runs: GITBOOK_RUNTIME: cloudflare shell: bash + - name: Upload the DO worker + uses: cloudflare/wrangler-action@v3.14.0 + with: + apiToken: ${{ inputs.apiToken }} + accountId: ${{ inputs.accountId }} + workingDirectory: ./ + wranglerVersion: '4.10.0' + environment: ${{ inputs.environment }} + command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc + - id: upload_server name: Upload server to Cloudflare uses: cloudflare/wrangler-action@v3.14.0 diff --git a/packages/gitbook-v2/openNext/customWorkers/default.js b/packages/gitbook-v2/openNext/customWorkers/default.js index bb673e1722..535c218167 100644 --- a/packages/gitbook-v2/openNext/customWorkers/default.js +++ b/packages/gitbook-v2/openNext/customWorkers/default.js @@ -1,5 +1,26 @@ import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js'; +import { DurableObject } from 'cloudflare:workers'; + +// Only needed to run locally, in prod we'll use the one from do.js +export class R2WriteBuffer extends DurableObject { + writePromise; + + async write(cacheKey, value) { + // We are already writing to this key + if (this.writePromise) { + return; + } + + this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value); + this.ctx.waitUntil( + this.writePromise.finally(() => { + this.writePromise = undefined; + }) + ); + } +} + export default { async fetch(request, env, ctx) { return runWithCloudflareRequestContext(request, env, ctx, async () => { diff --git a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc index 3fff07d7c0..e036db1c44 100644 --- a/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc +++ b/packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc @@ -29,6 +29,20 @@ "binding": "WORKER_SELF_REFERENCE", "service": "gitbook-open-v2-server-dev" } + ], + "durable_objects": { + "bindings": [ + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["R2WriteBuffer"] + } ] }, "preview": { @@ -48,9 +62,26 @@ "binding": "WORKER_SELF_REFERENCE", "service": "gitbook-open-v2-server-preview" } - ] - // No durable objects on preview, as they block the generation of preview URLs - // and we don't need tags invalidation on preview + ], + "durable_objects": { + "bindings": [ + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-preview" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-do-preview" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-preview" + } + ] + } }, "staging": { "r2_buckets": [ @@ -67,12 +98,20 @@ ], "durable_objects": { "bindings": [ - // We do not need to define migrations for external DOs, - // In fact, defining migrations for external DOs will crash + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-staging" + }, { "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache", - "script_name": "gitbook-open-v2-staging" + "script_name": "gitbook-open-v2-do-staging" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-staging" } ] }, @@ -98,11 +137,19 @@ "durable_objects": { "bindings": [ { - // We do not need to define migrations for external DOs, - // In fact, defining migrations for external DOs will crash + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-production" + }, + { "name": "NEXT_TAG_CACHE_DO_SHARDED", "class_name": "DOShardedTagCache", - "script_name": "gitbook-open-v2-production" + "script_name": "gitbook-open-v2-do-production" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-production" } ] }, diff --git a/packages/gitbook-v2/openNext/customWorkers/do.js b/packages/gitbook-v2/openNext/customWorkers/do.js new file mode 100644 index 0000000000..04f3cf3bec --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/do.js @@ -0,0 +1,38 @@ +// This worker only purposes it to host the different DO that we will need in the other workers. +import { DurableObject } from 'cloudflare:workers'; + +// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second +// so we need to buffer writes to the R2 bucket to avoid hitting this limit. +export class R2WriteBuffer extends DurableObject { + writePromise; + + async write(cacheKey, value) { + // We are already writing to this key + if (this.writePromise) { + return; + } + + this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value); + this.ctx.waitUntil( + this.writePromise.finally(() => { + this.writePromise = undefined; + }) + ); + } +} + +export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js'; + +export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js'; + +export default { + async fetch() { + // This worker does not handle any requests, it only provides Durable Objects + return new Response('This worker is not meant to handle requests directly', { + status: 400, + headers: { + 'Content-Type': 'text/plain', + }, + }); + }, +}; diff --git a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc new file mode 100644 index 0000000000..f5d2f726b8 --- /dev/null +++ b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc @@ -0,0 +1,127 @@ +{ + "main": "do.js", + "name": "gitbook-open-v2-do", + "compatibility_date": "2025-04-14", + "compatibility_flags": [ + "nodejs_compat", + "allow_importable_env", + "global_fetch_strictly_public" + ], + "observability": { + "enabled": true + }, + "env": { + "preview": { + "vars": { + "STAGE": "preview", + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + }, + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"] + } + ] + }, + "staging": { + "vars": { + "STAGE": "staging", + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-staging" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-staging-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + }, + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"] + } + ] + }, + "production": { + "vars": { + "NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true", + "STAGE": "production" + }, + "r2_buckets": [ + { + "binding": "NEXT_INC_CACHE_R2_BUCKET", + "bucket_name": "gitbook-open-v2-cache-production" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-prod-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + }, + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"] + } + ] + } + } +} diff --git a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc index 4870c89d54..09e48afcc0 100644 --- a/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc +++ b/packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc @@ -61,9 +61,26 @@ "binding": "DEFAULT_WORKER", "service": "gitbook-open-v2-server-preview" } - ] - // No durable objects on preview, as they block the generation of preview URLs - // and we don't need tags invalidation on preview + ], + "durable_objects": { + "bindings": [ + { + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-preview" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-do-preview" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-preview" + } + ] + } }, "staging": { "vars": { @@ -104,12 +121,19 @@ "durable_objects": { "bindings": [ { - "name": "NEXT_CACHE_DO_QUEUE", - "class_name": "DOQueueHandler" + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-staging" }, { "name": "NEXT_TAG_CACHE_DO_SHARDED", - "class_name": "DOShardedTagCache" + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-do-staging" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-staging" } ] }, @@ -165,12 +189,19 @@ "durable_objects": { "bindings": [ { - "name": "NEXT_CACHE_DO_QUEUE", - "class_name": "DOQueueHandler" + "name": "WRITE_BUFFER", + "class_name": "R2WriteBuffer", + "script_name": "gitbook-open-v2-do-production" }, { "name": "NEXT_TAG_CACHE_DO_SHARDED", - "class_name": "DOShardedTagCache" + "class_name": "DOShardedTagCache", + "script_name": "gitbook-open-v2-do-production" + }, + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler", + "script_name": "gitbook-open-v2-do-production" } ] }, diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts index d4a662af01..28d1d6b8a1 100644 --- a/packages/gitbook-v2/openNext/incrementalCache.ts +++ b/packages/gitbook-v2/openNext/incrementalCache.ts @@ -9,6 +9,8 @@ import type { } from '@opennextjs/aws/types/overrides.js'; import { getCloudflareContext } from '@opennextjs/cloudflare'; +import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types'; + export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET'; export const DEFAULT_PREFIX = 'incremental-cache'; @@ -79,12 +81,10 @@ class GitbookIncrementalCache implements IncrementalCache { }, async (span) => { span.setAttribute('cacheType', cacheType ?? 'cache'); - const r2 = getCloudflareContext().env[BINDING_NAME]; const localCache = await this.getCacheInstance(); - if (!r2) throw new Error('No R2 bucket'); try { - await r2.put(cacheKey, JSON.stringify(value)); + await this.writeToR2(cacheKey, JSON.stringify(value)); //TODO: Check if there is any places where we don't have tags // Ideally we should always have tags, but in case we don't, we need to decide how to handle it @@ -145,6 +145,22 @@ class GitbookIncrementalCache implements IncrementalCache { ); } + async writeToR2(key: string, value: string): Promise<void> { + const env = getCloudflareContext().env as { + WRITE_BUFFER: DurableObjectNamespace< + Rpc.DurableObjectBranded & { + write: (key: string, value: string) => Promise<void>; + } + >; + }; + const id = env.WRITE_BUFFER.idFromName(key); + + // A stub is a client used to invoke methods on the Durable Object + const stub = env.WRITE_BUFFER.get(id); + + await stub.write(key, value); + } + async getCacheInstance(): Promise<Cache> { if (this.localCache) return this.localCache; this.localCache = await caches.open('incremental-cache'); diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts index 173b5dd283..398ceee0d3 100644 --- a/packages/gitbook-v2/openNext/tagCache/middleware.ts +++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts @@ -6,6 +6,8 @@ import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-ca const originalTagCache = doShardedTagCache({ baseShardSize: 12, regionalCache: true, + // We can probably increase this value even further + regionalCacheTtlSec: 60, shardReplication: { numberOfSoftReplicas: 2, numberOfHardReplicas: 1, @@ -35,7 +37,7 @@ export default { } ); }, - hasBeenRevalidated: async (tags: string[]) => { + hasBeenRevalidated: async (tags: string[], lastModified?: number) => { const tagsToCheck = tags.filter(softTagFilter); if (tagsToCheck.length === 0) { // If we reach here, it probably means that there is an issue that we'll need to address. @@ -51,7 +53,7 @@ export default { name: tagsToCheck.join(', '), }, async () => { - const result = await originalTagCache.hasBeenRevalidated(tagsToCheck); + const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified); return result; } ); From 7d5a6d2255506fc0d4d314a9786a945cb4bf2727 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 2 Jun 2025 15:23:15 +0200 Subject: [PATCH 070/127] Fix hydration error on V2 (#3272) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/cold-buckets-divide.md | 5 +++ .../DocumentView/Table/RecordCard.tsx | 31 +++++++------- .../src/components/RootLayout/globals.css | 7 ++++ .../src/components/primitives/Link.tsx | 41 +++++++++++++++++++ 4 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 .changeset/cold-buckets-divide.md diff --git a/.changeset/cold-buckets-divide.md b/.changeset/cold-buckets-divide.md new file mode 100644 index 0000000000..1e528bad19 --- /dev/null +++ b/.changeset/cold-buckets-divide.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +fix nested a tag causing hydration error diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index 2a246d13c6..4218f760e4 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -4,7 +4,7 @@ import { SiteInsightsLinkPosition, } from '@gitbook/api'; -import { Link } from '@/components/primitives'; +import { LinkBox, LinkOverlay } from '@/components/primitives'; import { Image } from '@/components/utils'; import { resolveContentRef } from '@/lib/references'; import { type ClassValue, tcls } from '@/lib/tailwind'; @@ -44,7 +44,6 @@ export async function RecordCard( <div className={tcls( 'grid-area-1-1', - 'z-0', 'relative', 'grid', 'bg-tint-base', @@ -151,7 +150,6 @@ export async function RecordCard( 'rounded-md', 'straight-corners:rounded-none', 'dark:shadow-transparent', - 'z-0', 'before:pointer-events-none', 'before:grid-area-1-1', @@ -167,19 +165,22 @@ export async function RecordCard( if (target && targetRef) { return ( - <Link - href={target.href} - className={tcls(style, 'hover:before:ring-tint-12/5')} - insights={{ - type: 'link_click', - link: { - target: targetRef, - position: SiteInsightsLinkPosition.Content, - }, - }} - > + // We don't use `Link` directly here because we could end up in a situation where + // a link is rendered inside a link, which is not allowed in HTML. + // It causes an hydration error in React. + <LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}> + <LinkOverlay + href={target.href} + insights={{ + type: 'link_click', + link: { + target: targetRef, + position: SiteInsightsLinkPosition.Content, + }, + }} + /> {body} - </Link> + </LinkBox> ); } diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index 15c09614e3..2bb3e1a223 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -148,6 +148,13 @@ width: 100%; } } + + .elevate-link { + & a[href]:not(.link-overlay) { + position: relative; + z-index: 20; + } + } } html { diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx index 5cb026d3a5..cf9d2c24c4 100644 --- a/packages/gitbook/src/components/primitives/Link.tsx +++ b/packages/gitbook/src/components/primitives/Link.tsx @@ -3,6 +3,7 @@ import NextLink, { type LinkProps as NextLinkProps } from 'next/link'; import React from 'react'; +import { tcls } from '@/lib/tailwind'; import { type TrackEventInput, useTrackEvent } from '../Insights'; // Props from Next, which includes NextLinkProps and all the things anchor elements support. @@ -75,6 +76,46 @@ export const Link = React.forwardRef(function Link( ); }); +/** + * A box used to contain a link overlay. + * It is used to create a clickable area that can contain other elements. + */ +export const LinkBox = React.forwardRef(function LinkBox( + props: React.BaseHTMLAttributes<HTMLDivElement>, + ref: React.Ref<HTMLDivElement> +) { + const { children, className, ...domProps } = props; + return ( + <div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}> + {children} + </div> + ); +}); + +/** + * A link overlay that can be used to create a clickable area on top of other elements. + * It is used to create a link that covers the entire area of the element without encapsulating it in a link tag. + * This is useful to avoid nesting links inside links. + */ +export const LinkOverlay = React.forwardRef(function LinkOverlay( + props: LinkProps, + ref: React.Ref<HTMLAnchorElement> +) { + const { children, className, ...domProps } = props; + return ( + <Link + ref={ref} + {...domProps} + className={tcls( + 'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full', + className + )} + > + {children} + </Link> + ); +}); + /** * Check if a link is external, compared to an origin. */ From 0f867e533a65532730e014810a161753e81bbf9d Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 2 Jun 2025 17:40:04 +0200 Subject: [PATCH 071/127] bump OpenNext to latest (#3276) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- bun.lock | 6 +++--- packages/gitbook-v2/open-next.config.ts | 2 +- packages/gitbook-v2/openNext/queue/middleware.ts | 11 ++--------- packages/gitbook-v2/openNext/tagCache/middleware.ts | 3 +++ packages/gitbook-v2/package.json | 2 +- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 631adb8ce4..96f0f0d203 100644 --- a/bun.lock +++ b/bun.lock @@ -145,7 +145,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.0.4", + "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -791,9 +791,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 959834739a..1872309119 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -6,7 +6,7 @@ export default { wrapper: 'cloudflare-node', converter: 'edge', proxyExternalRequest: 'fetch', - queue: () => import('./openNext/queue/server').then((m) => m.default), + queue: () => import('./openNext/queue/middleware').then((m) => m.default), incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default), }, diff --git a/packages/gitbook-v2/openNext/queue/middleware.ts b/packages/gitbook-v2/openNext/queue/middleware.ts index 2a14dc1d8b..5ab486a975 100644 --- a/packages/gitbook-v2/openNext/queue/middleware.ts +++ b/packages/gitbook-v2/openNext/queue/middleware.ts @@ -2,20 +2,13 @@ import { trace } from '@/lib/tracing'; import type { Queue } from '@opennextjs/aws/types/overrides.js'; import { getCloudflareContext } from '@opennextjs/cloudflare'; import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; -import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue'; - -interface Env { - STAGE?: string; -} export default { name: 'GitbookISRQueue', send: async (msg) => { return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => { - const { ctx, env } = getCloudflareContext(); - const hasDurableObject = - (env as Env).STAGE !== 'dev' && (env as Env).STAGE !== 'preview'; - ctx.waitUntil(hasDurableObject ? memoryQueue.send(msg) : doQueue.send(msg)); + const { ctx } = getCloudflareContext(); + ctx.waitUntil(doQueue.send(msg)); }); }, } satisfies Queue; diff --git a/packages/gitbook-v2/openNext/tagCache/middleware.ts b/packages/gitbook-v2/openNext/tagCache/middleware.ts index 398ceee0d3..4f06d7896b 100644 --- a/packages/gitbook-v2/openNext/tagCache/middleware.ts +++ b/packages/gitbook-v2/openNext/tagCache/middleware.ts @@ -11,6 +11,9 @@ const originalTagCache = doShardedTagCache({ shardReplication: { numberOfSoftReplicas: 2, numberOfHardReplicas: 1, + regionalReplication: { + defaultRegion: 'enam', + }, }, }); diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 224d55e301..0b790a9bee 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,7 +5,7 @@ "dependencies": { "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.0.4", + "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", From 015615de8ab42bec73048aa7480193d8b1ac9444 Mon Sep 17 00:00:00 2001 From: Brett Jephson <brett@gitbook.io> Date: Tue, 3 Jun 2025 12:18:01 +0100 Subject: [PATCH 072/127] fix: defaultWidth/fullWidth options in images (#3274) --- .changeset/breezy-falcons-drop.md | 5 +++++ packages/gitbook/src/components/DocumentView/Images.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/breezy-falcons-drop.md diff --git a/.changeset/breezy-falcons-drop.md b/.changeset/breezy-falcons-drop.md new file mode 100644 index 0000000000..834e46bfca --- /dev/null +++ b/.changeset/breezy-falcons-drop.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Respect fullWidth and defaultWidth for images diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx index baaab50bdc..20018b6f49 100644 --- a/packages/gitbook/src/components/DocumentView/Images.tsx +++ b/packages/gitbook/src/components/DocumentView/Images.tsx @@ -29,7 +29,7 @@ export function Images(props: BlockProps<DocumentBlockImages>) { align === 'center' && 'justify-center', align === 'right' && 'justify-end', align === 'left' && 'justify-start', - isMultipleImages && ['grid', 'grid-flow-col', 'max-w-none'] + isMultipleImages && ['grid', 'grid-flow-col'] )} > {block.nodes.map((node: any, _i: number) => ( From c9373efdb514800809ccabf40fd8da2dad13a5ca Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 3 Jun 2025 15:18:04 +0200 Subject: [PATCH 073/127] Fix bold header links hover color (#3255) --- .changeset/strong-poets-move.md | 5 +++++ packages/gitbook/src/components/Header/HeaderLink.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/strong-poets-move.md diff --git a/.changeset/strong-poets-move.md b/.changeset/strong-poets-move.md new file mode 100644 index 0000000000..9914aa44c9 --- /dev/null +++ b/.changeset/strong-poets-move.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix bold header links hover color diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx index fcfb28c723..6b13fef714 100644 --- a/packages/gitbook/src/components/Header/HeaderLink.tsx +++ b/packages/gitbook/src/components/Header/HeaderLink.tsx @@ -159,7 +159,7 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese 'links-accent:py-0.5', // Prevent underline from being cut off at the bottom 'theme-bold:text-header-link', - 'theme-bold:hover:text-header-link' + 'theme-bold:hover:!text-header-link/7' ); } From 7b5a3616c48291577b15b805ecb490bf2310bbd7 Mon Sep 17 00:00:00 2001 From: Addison <42930383+addisonschultz@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:07:55 +0100 Subject: [PATCH 074/127] Add UTM tags to the preview site badge (#3277) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9702f88e3..8a9f5c2fdc 100644 --- a/README.md +++ b/README.md @@ -150,11 +150,11 @@ See `LICENSE` for more information. </p> ```md -[](https://gitbook.com/) +[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link) ``` ```html -<a href="https://gitbook.com"> +<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link"> <img src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1" /> From c730845e17c0858132c176439cf4a5b707cb0047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Wed, 4 Jun 2025 16:33:01 +0100 Subject: [PATCH 075/127] Fix missing title for button to close announcement banner (#3279) --- .changeset/sharp-hats-applaud.md | 5 +++++ .../src/components/Announcement/AnnouncementBanner.tsx | 4 ++++ packages/gitbook/src/components/Cookies/CookiesToast.tsx | 4 ++-- packages/gitbook/src/intl/translations/de.ts | 2 +- packages/gitbook/src/intl/translations/en.ts | 2 +- packages/gitbook/src/intl/translations/es.ts | 2 +- packages/gitbook/src/intl/translations/fr.ts | 2 +- packages/gitbook/src/intl/translations/ja.ts | 2 +- packages/gitbook/src/intl/translations/nl.ts | 2 +- packages/gitbook/src/intl/translations/no.ts | 2 +- packages/gitbook/src/intl/translations/pt-br.ts | 2 +- packages/gitbook/src/intl/translations/zh.ts | 2 +- 12 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 .changeset/sharp-hats-applaud.md diff --git a/.changeset/sharp-hats-applaud.md b/.changeset/sharp-hats-applaud.md new file mode 100644 index 0000000000..a3c5e8da9c --- /dev/null +++ b/.changeset/sharp-hats-applaud.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix missing title on button to close the announcement banner. diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx index 522bde0ca3..2bdd61caa7 100644 --- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx +++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx @@ -1,5 +1,6 @@ 'use client'; +import { tString, useLanguage } from '@/intl/client'; import * as storage from '@/lib/local-storage'; import type { ResolvedContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; @@ -18,6 +19,8 @@ export function AnnouncementBanner(props: { }) { const { announcement, contentRef } = props; + const language = useLanguage(); + const hasLink = announcement.link && contentRef?.href; const closeable = announcement.style !== 'danger'; @@ -81,6 +84,7 @@ export function AnnouncementBanner(props: { className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`} type="button" onClick={dismissAnnouncement} + title={tString(language, 'close')} > <Icon icon="close" className="size-4" /> </button> diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx index 13dc7605d0..8e1afc4c32 100644 --- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx +++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx @@ -72,7 +72,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) { <button type="button" onClick={() => setShow(false)} - aria-label={tString(language, 'cookies_close')} + aria-label={tString(language, 'close')} className={tcls( 'absolute', 'top-3', @@ -85,7 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) { 'rounded-sm', 'hover:bg-tint-hover' )} - title={tString(language, 'cookies_close')} + title={tString(language, 'close')} > <Icon icon="xmark" className={tcls('size-4')} /> </button> diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index b42da6c55f..9ed91a152c 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -40,7 +40,7 @@ export const de = { cookies_prompt_privacy: 'Datenschutzrichtlinie', cookies_accept: 'Akzeptieren', cookies_reject: 'Ablehnen', - cookies_close: 'Schließen', + close: 'Schließen', edit_on_git: 'Bearbeiten auf ${1}', notfound_title: 'Seite nicht gefunden', notfound: 'Die gesuchte Seite existiert nicht.', diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index df9cc50993..6fa837ebb8 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -40,7 +40,7 @@ export const en = { cookies_prompt_privacy: 'privacy policy', cookies_accept: 'Accept', cookies_reject: 'Reject', - cookies_close: 'Close', + close: 'Close', edit_on_git: 'Edit on ${1}', notfound_title: 'Page not found', notfound: "The page you are looking for doesn't exist.", diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 2d0d465652..d2ca1bb9d5 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -42,7 +42,7 @@ export const es: TranslationLanguage = { cookies_prompt_privacy: 'política de privacidad', cookies_accept: 'Aceptar', cookies_reject: 'Rechazar', - cookies_close: 'Cerrar', + close: 'Cerrar', edit_on_git: 'Editar en ${1}', notfound_title: 'Página no encontrada', notfound: 'La página que buscas no existe.', diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index 71d340332b..922f7554d0 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -42,7 +42,7 @@ export const fr: TranslationLanguage = { cookies_prompt_privacy: 'politique de confidentialité', cookies_accept: 'Accepter', cookies_reject: 'Rejeter', - cookies_close: 'Fermer', + close: 'Fermer', edit_on_git: 'Modifier sur ${1}', notfound_title: 'Page non trouvée', notfound: "La page que vous cherchez n'existe pas.", diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index f3f5480689..6eaa789ed8 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -42,7 +42,7 @@ export const ja: TranslationLanguage = { cookies_prompt_privacy: 'プライバシーポリシー', cookies_accept: '同意する', cookies_reject: '拒否する', - cookies_close: '閉じる', + close: '閉じる', edit_on_git: '${1}で編集', notfound_title: 'ページが見つかりません', notfound: 'お探しのページは存在しません。', diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index 6bfb45e9a6..d61bfb71d9 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -42,7 +42,7 @@ export const nl: TranslationLanguage = { cookies_prompt_privacy: 'privacyverklaring', cookies_accept: 'Accepteren', cookies_reject: 'Weigeren', - cookies_close: 'Sluiten', + close: 'Sluiten', edit_on_git: 'Bewerk op ${1}', notfound_title: 'Pagina niet gevonden', notfound: 'De pagina die je zoekt, bestaat niet.', diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index 6be6b413e3..ddd94e5466 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -42,7 +42,7 @@ export const no: TranslationLanguage = { cookies_prompt_privacy: 'personvernerklæringen', cookies_accept: 'Godta', cookies_reject: 'Avslå', - cookies_close: 'Lukk', + close: 'Lukk', edit_on_git: 'Rediger på ${1}', notfound_title: 'Siden ble ikke funnet', notfound: 'Siden du leter etter eksisterer ikke.', diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index 35a40cd45f..c3e2244785 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -40,7 +40,7 @@ export const pt_br = { cookies_prompt_privacy: 'política de privacidade', cookies_accept: 'Aceitar', cookies_reject: 'Rejeitar', - cookies_close: 'Fechar', + close: 'Fechar', edit_on_git: 'Editar no ${1}', notfound_title: 'Página não encontrada', notfound: 'A página que você está procurando não existe.', diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index 08efdddde6..096b0d8cb3 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -40,7 +40,7 @@ export const zh: TranslationLanguage = { cookies_prompt_privacy: '隐私政策', cookies_accept: '接受', cookies_reject: '拒绝', - cookies_close: '关闭', + close: '关闭', edit_on_git: '在${1}上编辑', notfound_title: '页面未找到', notfound: '您要找的页面不存在。', From 521052d84f0091091bdba97ab7853160e14d2fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Thu, 5 Jun 2025 20:43:15 +0100 Subject: [PATCH 076/127] Remove withoutConcurrentExecution and withCacheKey (#3281) --- .changeset/nervous-students-judge.md | 6 + packages/gitbook-v2/src/lib/data/api.ts | 1049 ++++++++--------- .../gitbook-v2/src/lib/data/cloudflare.ts | 12 - .../gitbook-v2/src/lib/data/memoize.test.ts | 52 - packages/gitbook-v2/src/lib/data/memoize.ts | 87 -- packages/gitbook/src/lib/openapi/fetch.ts | 10 +- 6 files changed, 483 insertions(+), 733 deletions(-) create mode 100644 .changeset/nervous-students-judge.md delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.test.ts delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts diff --git a/.changeset/nervous-students-judge.md b/.changeset/nervous-students-judge.md new file mode 100644 index 0000000000..a24d325b74 --- /dev/null +++ b/.changeset/nervous-students-judge.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": patch +"gitbook": patch +--- + +Fix concurrent execution in Vercel causing pages to not be attached to the proper tags. diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index f8ae6d2711..44313bfc57 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -8,9 +8,7 @@ import { import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; -import { getCloudflareRequestGlobal } from './cloudflare'; import { DataFetcherError, wrapDataFetcherError } from './errors'; -import { withCacheKey, withoutConcurrentExecution } from './memoize'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -196,613 +194,518 @@ export function createDataFetcher( }; } -const getUserById = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async (_, input: DataFetcherInput, params: { userId: string }) => { - 'use cache'; - return trace(`getUserById(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.users.getUserById(params.userId, { - ...noCacheFetchOptions, - }); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); +const getUserById = async (input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, }); - } - ) -); - -const getSpace = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); +}; - return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getSpaceById( - params.spaceId, - { - shareKey: params.shareKey, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - }); - } - ) -); - -const getChangeRequest = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }) - ); +const getSpace = async ( + input: DataFetcherInput, + params: { spaceId: string; shareKey: string | undefined } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); - return trace( - `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - return res.data; - }); + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, } ); - } - ) -); - -const getRevision = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } - ) => { - 'use cache'; - return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - }); - } - ) -); - -const getRevisionPages = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } - ) => { - 'use cache'; - return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data.pages; - }); - }); - } - ) -); - -const getRevisionFile = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string } - ) => { - 'use cache'; - return trace( - `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); +}; + +const getChangeRequest = async ( + input: DataFetcherInput, + params: { spaceId: string; changeRequestId: string } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); + + return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, } ); - } - ) -); - -const getRevisionPageMarkdown = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } - ) => { - 'use cache'; - return trace( - `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - format: 'markdown', - }, - { - ...noCacheFetchOptions, - } - ); - - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); - } - return res.data.markdown; - }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); + }); +}; + +const getRevision = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } +) => { + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, } ); - } - ) -); - -const getRevisionPageByPath = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string } - ) => { - 'use cache'; - return trace( - `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); +}; + +const getRevisionPages = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } +) => { + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, } ); - } - ) -); - -const getDocument = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { - 'use cache'; - return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getDocumentById( - params.spaceId, - params.documentId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); +}; + +const getRevisionFile = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; fileId: string } +) => { + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; }); } - ) -); - -const getComputedDocument = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - } - ) => { - 'use cache'; - cacheTag( - ...getComputedContentSourceCacheTags( + ); +}; + +const getRevisionPageMarkdown = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } +) => { + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, { - spaceId: params.spaceId, - organizationId: params.organizationId, + format: 'markdown', }, - params.source - ) - ); + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); - return trace( - `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getComputedDocument( - params.spaceId, - { - source: params.source, - seed: params.seed, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); } - ); + return res.data.markdown; + }); } - ) -); - -const getReusableContent = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string } - ) => { - 'use cache'; - return trace( - `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); + ); +}; + +const getRevisionPageByPath = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; path: string } +) => { + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - ) -); - -const getLatestOpenAPISpecVersionContent = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }) - ); + ); +}; - return trace( - `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); +const getDocument = async ( + input: DataFetcherInput, + params: { spaceId: string; documentId: string } +) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, } ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); +}; + +const getComputedDocument = async ( + input: DataFetcherInput, + params: { + spaceId: string; + organizationId: string; + source: ComputedContentSource; + seed: string; + } +) => { + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( + { + spaceId: params.spaceId, + organizationId: params.organizationId, + }, + params.source + ) + ); + + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getComputedDocument( + params.spaceId, + { + source: params.source, + seed: params.seed, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - ) -); - -const getPublishedContentSite = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + ); +}; - return trace( - `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - } - ); +const getReusableContent = async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; reusableContentId: string } +) => { + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - ) -); - -const getSiteRedirectBySource = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + ); +}; - return trace( - `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - } - ); +const getLatestOpenAPISpecVersionContent = async ( + input: DataFetcherInput, + params: { organizationId: string; slug: string } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'openapi', + organization: params.organizationId, + openAPISpec: params.slug, + }) + ); + + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - ) -); - -const getEmbedByUrl = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); + ); +}; - return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace( - params.spaceId, - { - url: params.url, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('weeks'); - return res.data; - }); +const getPublishedContentSite = async ( + input: DataFetcherInput, + params: { organizationId: string; siteId: string; siteShareKey: string | undefined } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); + + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; }); } - ) -); - -const searchSiteContent = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + ); +}; - return trace( - `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, scope } = params; - const api = apiClient(input); - const res = await api.orgs.searchSiteContent( - organizationId, - siteId, - { - query, - ...scope, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('hours'); - return res.data.items; - }); - } - ); +const getSiteRedirectBySource = async ( + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; + } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); + + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } - ) -); - -const renderIntegrationUi = withCacheKey( - withoutConcurrentExecution( - getCloudflareRequestGlobal, - async ( - _, - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'integration', - integration: params.integrationName, - }) + ); +}; + +const getEmbedByUrl = async (input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); + + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); + }); +}; + +const searchSiteContent = async ( + input: DataFetcherInput, + params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - return trace(`renderIntegrationUi(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { + return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; + const api = apiClient(input); + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('hours'); + return res.data.items; }); } - ) -); + ); +}; + +const renderIntegrationUi = async ( + input: DataFetcherInput, + params: { integrationName: string; request: RenderIntegrationUI } +) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) + ); + + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); +}; async function* streamAIResponse( input: DataFetcherInput, diff --git a/packages/gitbook-v2/src/lib/data/cloudflare.ts b/packages/gitbook-v2/src/lib/data/cloudflare.ts index ac3125603d..ca996690b6 100644 --- a/packages/gitbook-v2/src/lib/data/cloudflare.ts +++ b/packages/gitbook-v2/src/lib/data/cloudflare.ts @@ -11,15 +11,3 @@ export function getCloudflareContext() { return getCloudflareContextOpenNext(); } - -/** - * Return an object representing the current request. - */ -export function getCloudflareRequestGlobal() { - const context = getCloudflareContext(); - if (!context) { - return null; - } - - return context.cf; -} diff --git a/packages/gitbook-v2/src/lib/data/memoize.test.ts b/packages/gitbook-v2/src/lib/data/memoize.test.ts deleted file mode 100644 index 24c1014087..0000000000 --- a/packages/gitbook-v2/src/lib/data/memoize.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, expect, it, mock } from 'bun:test'; -import { AsyncLocalStorage } from 'node:async_hooks'; -import { withCacheKey, withoutConcurrentExecution } from './memoize'; - -describe('withoutConcurrentExecution', () => { - it('should memoize the function based on the cache key', async () => { - const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b); - const memoized = withoutConcurrentExecution(() => null, fn); - - const p1 = memoized('c1', 1, 2); - const p2 = memoized('c1', 1, 2); - const p3 = memoized('c3', 2, 3); - - expect(await p1).toBe(await p2); - expect(await p1).not.toBe(await p3); - expect(fn.mock.calls.length).toBe(2); - }); - - it('should support caching per request', async () => { - const fn = mock(async () => Math.random()); - - const request1 = { id: 'request1' }; - const request2 = { id: 'request2' }; - - const requestContext = new AsyncLocalStorage<{ id: string }>(); - - const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn); - - // Both in the same request - const promise1 = requestContext.run(request1, () => memoized('c1')); - const promise2 = requestContext.run(request1, () => memoized('c1')); - - // In a different request - const promise3 = requestContext.run(request2, () => memoized('c1')); - - expect(await promise1).toBe(await promise2); - expect(await promise1).not.toBe(await promise3); - expect(fn.mock.calls.length).toBe(2); - }); -}); - -describe('withCacheKey', () => { - it('should wrap the function by passing the cache key', async () => { - const fn = mock( - async (cacheKey: string, arg: { a: number; b: number }, c: number) => - `${cacheKey}, result=${arg.a + arg.b + c}` - ); - const memoized = withCacheKey(fn); - expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7'); - expect(fn.mock.calls.length).toBe(1); - }); -}); diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts deleted file mode 100644 index ff1ff5e841..0000000000 --- a/packages/gitbook-v2/src/lib/data/memoize.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Wrap a function by preventing concurrent executions of the same function. - * With a logic to work per-request in Cloudflare Workers. - */ -export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>( - getGlobalContext: () => object | null | undefined, - wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType> -): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> { - const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>(); - - return (key: string, ...args: ArgsType) => { - const globalContext = getGlobalContext() ?? globalThis; - - /** - * Cache storage that is scoped to the current request when executed in Cloudflare Workers, - * to avoid "Cannot perform I/O on behalf of a different request" errors. - */ - const promiseCache = - globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>(); - globalPromiseCache.set(globalContext, promiseCache); - - const concurrent = promiseCache.get(key); - if (concurrent) { - return concurrent; - } - - const promise = (async () => { - try { - const result = await wrapped(key, ...args); - return result; - } finally { - promiseCache.delete(key); - } - })(); - - promiseCache.set(key, promise); - - return promise; - }; -} - -/** - * Wrap a function by passing it a cache key that is computed from the function arguments. - */ -export function withCacheKey<ArgsType extends any[], ReturnType>( - wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> -): (...args: ArgsType) => Promise<ReturnType> { - return (...args: ArgsType) => { - const cacheKey = getCacheKey(args); - return wrapped(cacheKey, ...args); - }; -} - -/** - * Compute a cache key from the function arguments. - */ -function getCacheKey(args: any[]) { - return JSON.stringify(deepSortValue(args)); -} - -function deepSortValue(value: unknown): unknown { - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' || - value === null || - value === undefined - ) { - return value; - } - - if (Array.isArray(value)) { - return value.map(deepSortValue); - } - - if (value && typeof value === 'object') { - return Object.entries(value) - .map(([key, subValue]) => { - return [key, deepSortValue(subValue)] as const; - }) - .sort((a, b) => { - return a[0].localeCompare(b[0]); - }); - } - - return value; -} diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index b496bbe59f..889ea742ae 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -7,8 +7,6 @@ import type { OpenAPIWebhookBlock, ResolveOpenAPIBlockArgs, } from '@/lib/openapi/types'; -import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare'; -import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize'; import { assert } from 'ts-essentials'; import { resolveContentRef } from '../references'; import { isV2 } from '../v2'; @@ -48,7 +46,7 @@ export async function fetchOpenAPIFilesystem( function fetchFilesystem(url: string) { if (isV2()) { - return fetchFilesystemV2(url); + return fetchFilesystemUseCache(url); } return fetchFilesystemV1(url); @@ -68,12 +66,6 @@ const fetchFilesystemV1 = cache({ }, }); -const fetchFilesystemV2 = withCacheKey( - withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => { - return fetchFilesystemUseCache(url); - }) -); - const fetchFilesystemUseCache = async (url: string) => { 'use cache'; return fetchFilesystemUncached(url); From 7d3fe231959f17a3c4a08174d870b46a324eed97 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Sat, 7 Jun 2025 12:48:16 +0100 Subject: [PATCH 077/127] Add circular corners and depth styles (#3280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Samy Pessé <samypesse@gmail.com> --- .changeset/fair-crews-wink.md | 5 +++++ bun.lock | 12 ++++++------ package.json | 2 +- packages/cache-tags/package.json | 2 +- packages/gitbook-v2/package.json | 2 +- packages/gitbook/e2e/internal.spec.ts | 11 +++++++++++ packages/gitbook/e2e/util.ts | 2 ++ packages/gitbook/package.json | 2 +- .../Announcement/AnnouncementBanner.tsx | 4 ++-- .../src/components/Cookies/CookiesToast.tsx | 3 +++ .../src/components/DocumentView/Block.tsx | 6 ++++++ .../gitbook/src/components/DocumentView/Hint.tsx | 1 + .../src/components/DocumentView/Images.tsx | 9 ++------- .../components/DocumentView/OpenAPI/scalar.css | 2 +- .../components/DocumentView/OpenAPI/style.css | 10 +++++----- .../components/DocumentView/Table/RecordCard.tsx | 7 +++++-- .../components/DocumentView/Table/ViewGrid.tsx | 3 ++- .../PageAside/AsideSectionHighlight.tsx | 1 + .../components/PageAside/ScrollSectionsList.tsx | 1 + .../RootLayout/CustomizationRootLayout.tsx | 6 ++---- .../src/components/Search/SearchAskAnswer.tsx | 1 + .../src/components/Search/SearchButton.tsx | 6 ++++++ .../src/components/Search/SearchModal.tsx | 4 +++- .../components/Search/SearchPageResultItem.tsx | 1 + .../Search/SearchQuestionResultItem.tsx | 1 + .../Search/SearchSectionResultItem.tsx | 1 + .../components/SiteSections/SiteSectionList.tsx | 16 +++++++++++++++- .../components/SiteSections/SiteSectionTabs.tsx | 6 +++--- .../components/TableOfContents/PageLinkItem.tsx | 1 + .../TableOfContents/TableOfContents.tsx | 3 ++- .../TableOfContents/ToggleableLinkItem.tsx | 2 +- .../src/components/TableOfContents/Trademark.tsx | 1 + .../src/components/ThemeToggler/ThemeToggler.tsx | 1 + .../gitbook/src/components/primitives/Button.tsx | 9 +++++++-- .../gitbook/src/components/primitives/Card.tsx | 1 + packages/gitbook/src/lib/utils.ts | 1 + packages/gitbook/tailwind.config.ts | 5 ++++- packages/react-contentkit/package.json | 2 +- 38 files changed, 110 insertions(+), 43 deletions(-) create mode 100644 .changeset/fair-crews-wink.md diff --git a/.changeset/fair-crews-wink.md b/.changeset/fair-crews-wink.md new file mode 100644 index 0000000000..817854d1d7 --- /dev/null +++ b/.changeset/fair-crews-wink.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Add circular corners and depth styling diff --git a/bun.lock b/bun.lock index 96f0f0d203..cf33dddcca 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -51,7 +51,7 @@ "name": "gitbook", "version": "0.12.0", "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -143,7 +143,7 @@ "name": "gitbook-v2", "version": "0.3.0", "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", @@ -202,7 +202,7 @@ "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -260,7 +260,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -625,7 +625,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="], + "@gitbook/api": ["@gitbook/api@0.118.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5bpvXyGNsMOn1Ee7uzohDz/yOgxpVyLZMLu6THYwbG9UeY6BFsWISeqTw03COCX42rVLU5zFReDxRTb7lfZtCw=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index 5398e52f9f..8ee2b33518 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index 8850dafbd6..37e243bdb6 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 0b790a9bee..6eaa3a37c3 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "private": true, "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index a70a7e5d8d..17f03b9318 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -1,6 +1,7 @@ import { CustomizationBackground, CustomizationCorners, + CustomizationDepth, CustomizationHeaderPreset, CustomizationIconsStyle, CustomizationSidebarListStyle, @@ -817,6 +818,16 @@ const testCases: TestsCase[] = [ }), run: waitForCookiesDialog, }, + { + name: `With flat and circular corners - Theme mode ${themeMode}`, + url: getCustomizationURL({ + styling: { + depth: CustomizationDepth.Flat, + corners: CustomizationCorners.Circular, + }, + }), + run: waitForCookiesDialog, + }, ]), }, { diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 61113f8264..f54b70fbe2 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -3,6 +3,7 @@ import { CustomizationBackground, CustomizationCorners, CustomizationDefaultFont, + CustomizationDepth, type CustomizationHeaderItem, CustomizationHeaderPreset, CustomizationIconsStyle, @@ -278,6 +279,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin dangerColor: { light: '#FB2C36', dark: '#FB2C36' }, successColor: { light: '#00C950', dark: '#00C950' }, corners: CustomizationCorners.Rounded, + depth: CustomizationDepth.Subtle, font: CustomizationDefaultFont.Inter, background: CustomizationBackground.Plain, icons: CustomizationIconsStyle.Regular, diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index a3b9da821a..8443bb615a 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" }, "dependencies": { - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx index 2bdd61caa7..2b07504f3f 100644 --- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx +++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx @@ -34,7 +34,7 @@ export function AnnouncementBanner(props: { <Tag href={contentRef?.href ?? ''} className={tcls( - 'flex w-full items-start justify-center overflow-hidden rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors', + 'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors', style.container, closeable && 'pr-12', hasLink && style.hover @@ -81,7 +81,7 @@ export function AnnouncementBanner(props: { </Tag> {closeable ? ( <button - className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`} + className={`absolute top-0 right-4 mt-2 mr-2 rounded circular-corners:rounded-lg straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`} type="button" onClick={dismissAnnouncement} title={tString(language, 'close')} diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx index 8e1afc4c32..281e6ad911 100644 --- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx +++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx @@ -47,9 +47,11 @@ export function CookiesToast(props: { privacyPolicy?: string }) { 'bg-tint-base', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', 'ring-1', 'ring-tint-subtle', 'shadow-1xs', + 'depth-flat:shadow-none', 'p-4', 'pr-8', 'bottom-4', @@ -83,6 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) { 'justify-center', 'items-center', 'rounded-sm', + 'circular-corners:rounded-full', 'hover:bg-tint-hover' )} title={tString(language, 'close')} diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index 60998b50b1..3dba09bf56 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -113,6 +113,9 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) { case 'code-line': case 'tabs-item': throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); + case 'columns': + case 'column': + return null; default: return nullIfNever(block); } @@ -177,6 +180,9 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue } case 'code-line': case 'tabs-item': throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); + case 'columns': + case 'column': + return null; default: return nullIfNever(block); } diff --git a/packages/gitbook/src/components/DocumentView/Hint.tsx b/packages/gitbook/src/components/DocumentView/Hint.tsx index 59ff1e0ed7..79961ee172 100644 --- a/packages/gitbook/src/components/DocumentView/Hint.tsx +++ b/packages/gitbook/src/components/DocumentView/Hint.tsx @@ -23,6 +23,7 @@ export function Hint(props: BlockProps<DocumentBlockHint>) { 'rounded-md', hasHeading ? 'rounded-l' : null, 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', 'overflow-hidden', hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container, diff --git a/packages/gitbook/src/components/DocumentView/Images.tsx b/packages/gitbook/src/components/DocumentView/Images.tsx index 20018b6f49..91d869002e 100644 --- a/packages/gitbook/src/components/DocumentView/Images.tsx +++ b/packages/gitbook/src/components/DocumentView/Images.tsx @@ -1,9 +1,4 @@ -import type { - DocumentBlockImage, - DocumentBlockImageDimension, - DocumentBlockImages, - JSONDocument, -} from '@gitbook/api'; +import type { DocumentBlockImage, DocumentBlockImages, JSONDocument, Length } from '@gitbook/api'; import { Image, type ImageResponsiveSize } from '@/components/utils'; import { resolveContentRef } from '@/lib/references'; @@ -119,7 +114,7 @@ async function ImageBlock(props: { * When using relative values, the converted dimension will be relative to the parent element's size. */ function getImageDimension<DefaultValue>( - dimension: DocumentBlockImageDimension | undefined, + dimension: Length | undefined, defaultValue: DefaultValue ): string | DefaultValue { if (typeof dimension === 'number') { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css index 8096a9fbef..b7eec226f9 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css @@ -271,7 +271,7 @@ body { } .scalar-activate-button { @apply flex gap-2 items-center; - @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none place-self-start; + @apply bg-primary-solid text-contrast-primary-solid hover:bg-primary-solid-hover hover:text-contrast-primary-solid-hover contrast-more:ring-1 rounded-md straight-corners:rounded-none circular-corners:rounded-full circular-corners:px-3 place-self-start; @apply ring-1 ring-tint hover:ring-tint-hover; @apply shadow-sm shadow-tint dark:shadow-tint-1 hover:shadow-md active:shadow-none; @apply contrast-more:ring-tint-12 contrast-more:hover:ring-2 contrast-more:hover:ring-tint-12; diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 213d12dec5..1a88b4faa6 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -481,7 +481,7 @@ .openapi-panel, .openapi-codesample, .openapi-response-examples { - @apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm; + @apply border rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm; } .openapi-panel pre, @@ -571,7 +571,7 @@ body:has(.openapi-select-popover) { } .openapi-select-popover { - @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none; + @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md circular-corners:rounded-xl straight-corners:rounded-none; @apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1; } @@ -680,7 +680,7 @@ body:has(.openapi-select-popover) { /* Disclosure group */ .openapi-disclosure-group { - @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative; + @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none circular-corners:last:rounded-b-xl first:rounded-t-md straight-corners:first:rounded-none circular-corners:first:rounded-t-xl first:border-t relative; } .openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) { @@ -786,7 +786,7 @@ body:has(.openapi-select-popover) { } .openapi-disclosure-trigger-label { - @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs; + @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none circular-corners:rounded-xl flex flex-row gap-1 items-center text-xs; } .openapi-disclosure-trigger-label span { @@ -848,7 +848,7 @@ body:has(.openapi-select-popover) { } .openapi-tooltip { - @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px]; + @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none circular-corners:rounded-lg font-medium px-1.5 py-0.5 shadow-sm text-[13px]; } .openapi-tooltip svg { diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index 4218f760e4..4bc1e0ce85 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -50,8 +50,9 @@ export async function RecordCard( 'w-[calc(100%+2px)]', 'h-[calc(100%+2px)]', 'inset-[-1px]', - 'rounded-[7px]', + 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', 'overflow-hidden', '[&_.heading>div:first-child]:hidden', '[&_.heading>div]:text-[.8em]', @@ -147,8 +148,10 @@ export async function RecordCard( 'grid', 'shadow-1xs', 'shadow-tint-9/1', - 'rounded-md', + 'depth-flat:shadow-none', + 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', 'dark:shadow-transparent', 'before:pointer-events-none', diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx index 9f2b98ba44..606b2305b5 100644 --- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx @@ -38,7 +38,8 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) { className={tcls( tableWidth, styles.rowGroup, - 'straight-corners:rounded-none' + 'straight-corners:rounded-none', + 'circular-corners:rounded-xl' )} > <div role="row" className={tcls('flex', 'w-full')}> diff --git a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx index 41775ca782..4b18d3d909 100644 --- a/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx +++ b/packages/gitbook/src/components/PageAside/AsideSectionHighlight.tsx @@ -30,6 +30,7 @@ export function AsideSectionHighlight({ 'rounded-md', 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', 'sidebar-list-line:rounded-l-none', 'sidebar-list-pill:bg-primary', diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx index d9b02ebdf8..519c1ea15b 100644 --- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx +++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx @@ -82,6 +82,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) { 'rounded-md', 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', 'sidebar-list-line:rounded-l-none', 'hover:bg-tint-hover', diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx index c6f141425a..ffaad1fff8 100644 --- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx +++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx @@ -1,5 +1,4 @@ import { - CustomizationCorners, CustomizationHeaderPreset, CustomizationIconsStyle, CustomizationSidebarBackgroundStyle, @@ -77,14 +76,13 @@ export async function CustomizationRootLayout(props: { customization.header.preset === CustomizationHeaderPreset.None ? null : 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling - customization.styling.corners === CustomizationCorners.Straight - ? ' straight-corners' - : '', + customization.styling.corners && `${customization.styling.corners}-corners`, 'theme' in customization.styling && `theme-${customization.styling.theme}`, tintColor ? ' tint' : 'no-tint', sidebarStyles.background && `sidebar-${sidebarStyles.background}`, sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`, 'links' in customization.styling && `links-${customization.styling.links}`, + 'depth' in customization.styling && `depth-${customization.styling.depth}`, fontNotoColorEmoji.variable, ibmPlexMono.variable, fontData.type === 'default' ? fontData.variable : 'font-custom', diff --git a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx index 8a7dce493d..f7c4b1fc8e 100644 --- a/packages/gitbook/src/components/Search/SearchAskAnswer.tsx +++ b/packages/gitbook/src/components/Search/SearchAskAnswer.tsx @@ -176,6 +176,7 @@ function AnswerFollowupQuestions(props: { followupQuestions: string[] }) { 'py-2', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', 'text-tint', 'hover:bg-tint-hover', 'focus-within:bg-tint-hover' diff --git a/packages/gitbook/src/components/Search/SearchButton.tsx b/packages/gitbook/src/components/Search/SearchButton.tsx index ab8516d059..7bcc4f7481 100644 --- a/packages/gitbook/src/components/Search/SearchButton.tsx +++ b/packages/gitbook/src/components/Search/SearchButton.tsx @@ -45,21 +45,25 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV 'w-full', 'py-2', 'px-3', + 'circular-corners:px-4', 'gap-2', 'bg-tint-base', 'ring-1', 'ring-tint-12/2', + 'depth-flat:ring-tint-subtle', 'shadow-sm', 'shadow-tint-12/3', 'dark:shadow-none', + 'depth-flat:shadow-none', 'text-tint', 'rounded-lg', 'straight-corners:rounded-sm', + 'circular-corners:rounded-full', 'contrast-more:ring-tint-12', 'contrast-more:text-tint-strong', @@ -68,10 +72,12 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV 'hover:bg-tint-subtle', 'hover:shadow-md', 'hover:scale-102', + 'depth-flat:hover:scale-100', 'hover:ring-tint-hover', 'hover:text-tint-strong', 'focus:shadow-md', 'focus:scale-102', + 'depth-flat:focus:scale-100', 'focus:ring-tint-hover', 'focus:text-tint-strong', diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx index bdc747241a..c8046ca333 100644 --- a/packages/gitbook/src/components/Search/SearchModal.tsx +++ b/packages/gitbook/src/components/Search/SearchModal.tsx @@ -225,9 +225,11 @@ function SearchModalBody( 'w-full', 'rounded-lg', 'straight-corners:rounded-sm', + 'circular-corners:rounded-2xl', 'ring-1', - 'ring-tint-hover', + 'ring-tint', 'shadow-2xl', + 'depth-flat:shadow-none', 'overflow-hidden', 'dark:ring-inset', 'dark:ring-tint' diff --git a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx index 6b4d0e1c0e..aa97e621a4 100644 --- a/packages/gitbook/src/components/Search/SearchPageResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchPageResultItem.tsx @@ -108,6 +108,7 @@ export const SearchPageResultItem = React.forwardRef(function SearchPageResultIt 'p-2', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6'] )} > diff --git a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx index 83f585c82b..787632a805 100644 --- a/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchQuestionResultItem.tsx @@ -72,6 +72,7 @@ export const SearchQuestionResultItem = React.forwardRef(function SearchQuestion 'rounded', 'self-center', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', active ? ['bg-primary-solid', 'text-contrast-primary-solid'] : ['opacity-6'] )} > diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx index ee2daa1ffb..960b1eb38a 100644 --- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx @@ -77,6 +77,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe 'p-2', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', 'bg-primary-solid', 'text-contrast-primary-solid', 'hidden', diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx index 6026296839..dc78a3c3c2 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx @@ -84,7 +84,21 @@ export function SiteSectionListItem(props: { href={section.url} aria-current={isActive && 'page'} className={tcls( - 'group/section-link flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none px-3 py-2 transition-all hover:bg-tint-hover hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-tint', + 'group/section-link', + 'flex', + 'flex-row', + 'items-center', + 'gap-3', + 'rounded-md', + 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', + 'px-3', + 'py-2', + 'transition-all', + 'hover:bg-tint-hover', + 'hover:text-tint-strong', + 'contrast-more:hover:ring-1', + 'contrast-more:hover:ring-tint', isActive ? 'font-semibold text-primary-subtle hover:bg-primary-hover hover:text-primary contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:hover:ring-1 contrast-more:hover:ring-primary-hover' : null, diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index 8496845507..8d6df8eafa 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -133,7 +133,7 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) { }} > <NavigationMenu.Viewport - className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6" + className="relative mt-3 h-[var(--radix-navigation-menu-viewport-height)] w-[calc(100vw_-_2rem)] origin-[top_center] overflow-hidden rounded-lg straight-corners:rounded-sm bg-tint-base depth-flat:shadow-none shadow-lg shadow-tint-10/6 ring-1 ring-tint-subtle duration-250 data-[state=closed]:duration-150 motion-safe:transition-[width,_height,_transform] data-[state=closed]:motion-safe:animate-scaleOut data-[state=open]:motion-safe:animate-scaleIn md:mx-0 md:w-[var(--radix-navigation-menu-viewport-width)] dark:shadow-tint-1/6" style={{ translate: undefined /* don't move this to a Tailwind class as Radix renders viewport incorrectly for a few frames */, @@ -157,7 +157,7 @@ const SectionTab = React.forwardRef(function SectionTab( ref={ref} {...rest} className={tcls( - 'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1', + 'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1', isActive ? 'text-primary-subtle' : 'text-tint hover:bg-tint-hover hover:text-tint-strong' @@ -186,7 +186,7 @@ const SectionGroupTab = React.forwardRef(function SectionGroupTab( ref={ref} {...rest} className={tcls( - 'group relative my-2 flex select-none items-center justify-between rounded straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default', + 'group relative my-2 flex select-none items-center justify-between rounded circular-corners:rounded-full straight-corners:rounded-none px-3 py-1 transition-colors hover:cursor-default', isActive ? 'text-primary-subtle' : 'text-tint hover:bg-tint-hover hover:text-tint-strong' diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index 636cc514d9..35f350a0c3 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -30,6 +30,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git 'text-tint-strong/7', 'rounded-md', 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', 'before:content-none', 'font-normal', 'hover:bg-tint', diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 7a6993fde1..73e9769a92 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -86,7 +86,8 @@ export function TableOfContents(props: { 'page-no-toc:!bg-transparent', 'sidebar-filled:rounded-xl', - 'straight-corners:rounded-none' + 'straight-corners:rounded-none', + '[html.sidebar-filled.circular-corners_&]:page-has-toc:rounded-3xl' )} > {innerHeader && <div className="px-5 *:my-4">{innerHeader}</div>} diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 38d6674514..92502433ba 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -69,7 +69,7 @@ function LinkItem( className={tcls( 'group/toclink relative transition-colors', 'flex flex-row justify-between', - 'rounded-md straight-corners:rounded-none p-1.5 pl-3', + 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12', 'before:contents[] before:-left-px before:absolute before:inset-y-0', diff --git a/packages/gitbook/src/components/TableOfContents/Trademark.tsx b/packages/gitbook/src/components/TableOfContents/Trademark.tsx index 19f5aed660..aa2d37c0ac 100644 --- a/packages/gitbook/src/components/TableOfContents/Trademark.tsx +++ b/packages/gitbook/src/components/TableOfContents/Trademark.tsx @@ -102,6 +102,7 @@ export function TrademarkLink(props: { 'rounded-lg', 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', 'hover:bg-tint', 'hover:text-tint-strong', diff --git a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx index af943dcbce..93f15e47c4 100644 --- a/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx +++ b/packages/gitbook/src/components/ThemeToggler/ThemeToggler.tsx @@ -69,6 +69,7 @@ function ThemeButton(props: { 'p-2', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', 'transition-all', 'text-tint', 'contrast-more:text-tint-strong', diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx index b7edac7ab0..58697d5b7f 100644 --- a/packages/gitbook/src/components/primitives/Button.tsx +++ b/packages/gitbook/src/components/primitives/Button.tsx @@ -40,8 +40,10 @@ const variantClasses = { ], secondary: [ 'bg-tint', + 'depth-flat:bg-transparent', 'text-tint', 'hover:bg-tint-hover', + 'depth-flat:hover:bg-tint-hover', 'hover:text-primary', 'contrast-more:bg-tint-subtle', ], @@ -60,8 +62,8 @@ export function Button({ ...rest }: ButtonProps & { target?: HTMLAttributeAnchorTarget }) { const sizes = { - default: ['text-base', 'px-4', 'py-2'], - medium: ['text-sm', 'px-3', 'py-1.5'], + default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'], + medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'], small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'], }; @@ -74,6 +76,7 @@ export function Button({ 'gap-2', 'rounded-md', 'straight-corners:rounded-none', + 'circular-corners:rounded-full', // 'place-self-start', 'ring-1', @@ -85,12 +88,14 @@ export function Button({ 'dark:shadow-tint-1', 'hover:shadow-md', 'active:shadow-none', + 'depth-flat:shadow-none', 'contrast-more:ring-tint-12', 'contrast-more:hover:ring-2', 'contrast-more:hover:ring-tint-12', 'hover:scale-104', + 'depth-flat:hover:scale-100', 'active:scale-100', 'transition-all', diff --git a/packages/gitbook/src/components/primitives/Card.tsx b/packages/gitbook/src/components/primitives/Card.tsx index bb40142f31..a796ba9eff 100644 --- a/packages/gitbook/src/components/primitives/Card.tsx +++ b/packages/gitbook/src/components/primitives/Card.tsx @@ -30,6 +30,7 @@ export async function Card( 'ring-tint-subtle', 'rounded', 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', 'px-5', 'py-3', 'transition-shadow', diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts index 0db784cf2d..d7a98610d0 100644 --- a/packages/gitbook/src/lib/utils.ts +++ b/packages/gitbook/src/lib/utils.ts @@ -17,6 +17,7 @@ export function defaultCustomization(): api.SiteCustomizationSettings { background: api.CustomizationBackground.Plain, icons: api.CustomizationIconsStyle.Regular, links: api.CustomizationLinksStyle.Default, + depth: api.CustomizationDepth.Subtle, sidebar: { background: api.CustomizationSidebarBackgroundStyle.Default, list: api.CustomizationSidebarListStyle.Default, diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 9d248c9a40..7981c337b2 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -483,7 +483,10 @@ const config: Config = { theme: ['theme-clean', 'theme-muted', 'theme-bold', 'theme-gradient'], // Corner styles - corner: ['straight-corners'], + corner: ['straight-corners', 'rounded-corners', 'circular-corners'], + + // Depth styles + depth: ['depth-flat', 'depth-subtle'], // Link styles links: ['links-default', 'links-accent'], diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index 09e6cc8e7c..16eab1f683 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.115.0", + "@gitbook/api": "^0.118.0", "@gitbook/icons": "workspace:*" }, "peerDependencies": { From 6aa3ff9c5efa2ebd6dd1fb96683c4d5b4f8d2cd4 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Sun, 8 Jun 2025 00:38:41 +0100 Subject: [PATCH 078/127] Fix various styling issues (#3283) --- .changeset/big-gorillas-perform.md | 9 +++++++++ packages/gitbook/src/components/DocumentView/Caption.tsx | 2 +- .../gitbook/src/components/DocumentView/StepperStep.tsx | 6 +++--- .../gitbook/src/components/Header/SpacesDropdown.tsx | 2 +- packages/gitbook/src/components/PageBody/PageCover.tsx | 2 +- .../src/components/PageBody/PageFooterNavigation.tsx | 1 + .../src/components/TableOfContents/TableOfContents.tsx | 1 + 7 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/big-gorillas-perform.md diff --git a/.changeset/big-gorillas-perform.md b/.changeset/big-gorillas-perform.md new file mode 100644 index 0000000000..88ec555a00 --- /dev/null +++ b/.changeset/big-gorillas-perform.md @@ -0,0 +1,9 @@ +--- +"gitbook": patch +--- + +Fix three small visual issues + +- Fix sidebar showing on `no-toc` pages in the gradient theme +- Fix variant selector truncating incorrectly in header when sections are present +- Fix page cover alignment on `lg` screens without TOC diff --git a/packages/gitbook/src/components/DocumentView/Caption.tsx b/packages/gitbook/src/components/DocumentView/Caption.tsx index f7649e68a8..c7075e6bad 100644 --- a/packages/gitbook/src/components/DocumentView/Caption.tsx +++ b/packages/gitbook/src/components/DocumentView/Caption.tsx @@ -42,7 +42,7 @@ export function Caption( 'after:pointer-events-none', fit ? 'w-fit' : null, withBorder - ? 'rounded straight-corners:rounded-none after:border-tint-subtle after:border after:rounded straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none' + ? 'rounded circular-corners:rounded-2xl straight-corners:rounded-none after:border-tint-subtle after:border after:rounded circular-corners:after:rounded-2xl straight-corners:after:rounded-none dark:after:mix-blend-plus-lighter after:pointer-events-none' : null, ], style, diff --git a/packages/gitbook/src/components/DocumentView/StepperStep.tsx b/packages/gitbook/src/components/DocumentView/StepperStep.tsx index 46e70ed603..5c7456ad60 100644 --- a/packages/gitbook/src/components/DocumentView/StepperStep.tsx +++ b/packages/gitbook/src/components/DocumentView/StepperStep.tsx @@ -34,13 +34,13 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) { <div className="relative select-none"> <div className={tcls( - 'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-subtle tabular-nums', - 'font-medium text-primary' + 'can-override-bg can-override-text flex size-[calc(1.75rem+1px)] items-center justify-center rounded-full bg-primary-solid theme-muted:bg-primary-subtle tabular-nums', + 'font-medium text-contrast-primary-solid theme-muted:text-primary' )} > {index + 1} </div> - <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-subtle" /> + <div className="can-override-bg absolute top-9 bottom-2 left-[0.875rem] w-px bg-primary-7 theme-muted:bg-primary-subtle" /> </div> <Blocks {...contextProps} diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 3fc1335230..1523774493 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -62,7 +62,7 @@ export function SpacesDropdown(props: { className )} > - <span className={tcls('line-clamp-1', 'grow')}>{siteSpace.title}</span> + <span className={tcls('truncate', 'grow')}>{siteSpace.title}</span> <DropdownChevron /> </div> } diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index d434d5eba9..29fb1a4923 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -37,7 +37,7 @@ export async function PageCover(props: { 'lg:ml-0', !page.layout.tableOfContents && context.customization.header.preset !== 'none' - ? 'lg:-ml-64' + ? 'xl:-ml-64' : null, ] : ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8'] diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx index 620cb5970d..b0190b6b18 100644 --- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx +++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx @@ -107,6 +107,7 @@ function NavigationCard( 'border', 'border-tint-subtle', 'rounded', + 'circular-corners:rounded-2xl', 'straight-corners:rounded-none', 'hover:border-primary', 'text-pretty', diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 73e9769a92..1fb8b59442 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -84,6 +84,7 @@ export function TableOfContents(props: { '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', '[html.sidebar-filled.theme-gradient_&]:border', 'page-no-toc:!bg-transparent', + 'page-no-toc:!border-none', 'sidebar-filled:rounded-xl', 'straight-corners:rounded-none', From 33726c88a04f7c833204eddb6816934b47923769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Sun, 8 Jun 2025 12:13:14 +0200 Subject: [PATCH 079/127] Generate a llms-full version of the docs (#3285) --- .changeset/orange-hounds-sparkle.md | 5 + bun.lock | 41 +++- .../[siteData]/llms-full.txt/route.ts | 14 ++ packages/gitbook/e2e/internal.spec.ts | 32 +++ packages/gitbook/package.json | 9 +- packages/gitbook/src/lib/urls.ts | 14 ++ packages/gitbook/src/routes/llms-full.ts | 209 ++++++++++++++++++ packages/gitbook/src/routes/llms.ts | 2 +- 8 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 .changeset/orange-hounds-sparkle.md create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts create mode 100644 packages/gitbook/src/routes/llms-full.ts diff --git a/.changeset/orange-hounds-sparkle.md b/.changeset/orange-hounds-sparkle.md new file mode 100644 index 0000000000..28f2b6aa6e --- /dev/null +++ b/.changeset/orange-hounds-sparkle.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Generate a llms-full.txt version of the docs site diff --git a/bun.lock b/bun.lock index cf33dddcca..5a2653c915 100644 --- a/bun.lock +++ b/bun.lock @@ -80,14 +80,19 @@ "jwt-decode": "^4.0.0", "katex": "^0.16.9", "mathjax": "^3.2.2", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", "mdast-util-to-markdown": "^2.1.2", "memoizee": "^0.4.17", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm": "^3.0.0", "next": "14.2.26", "next-themes": "^0.2.1", "nuqs": "^2.2.3", "object-hash": "^3.0.0", "openapi-types": "^12.1.3", - "p-map": "^7.0.0", + "p-map": "^7.0.3", "parse-cache-control": "^1.0.1", "partial-json": "^0.1.7", "react": "^19.0.0", @@ -104,6 +109,8 @@ "tailwind-merge": "^2.2.0", "tailwind-shades": "^1.1.2", "unified": "^11.0.5", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.0.0", "url-join": "^5.0.0", "usehooks-ts": "^3.1.0", "zod": "^3.24.2", @@ -1809,6 +1816,8 @@ "fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="], + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], @@ -1835,6 +1844,8 @@ "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], @@ -2181,9 +2192,11 @@ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA=="], - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], - "mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], @@ -2227,6 +2240,8 @@ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA=="], + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], @@ -2395,7 +2410,7 @@ "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="], + "p-map": ["p-map@7.0.3", "", {}, "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -2829,6 +2844,8 @@ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], @@ -4115,14 +4132,20 @@ "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + "mdast-util-gfm-footnote/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + "mdast-util-gfm-strikethrough/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + "mdast-util-gfm-strikethrough/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + "mdast-util-gfm-table/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + "mdast-util-gfm-table/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + "mdast-util-gfm-task-list-item/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + "mdast-util-gfm-task-list-item/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], "meow/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], @@ -4191,6 +4214,10 @@ "read-yaml-file/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "remark-gfm/mdast-util-gfm": ["mdast-util-gfm@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw=="], + + "remark-parse/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + "remark-stringify/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], @@ -5019,6 +5046,10 @@ "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "remark-gfm/mdast-util-gfm/mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA=="], + + "remark-gfm/mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts new file mode 100644 index 0000000000..7702d6101c --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms-full.txt/route.ts @@ -0,0 +1,14 @@ +import type { NextRequest } from 'next/server'; + +import { serveLLMsFullTxt } from '@/routes/llms-full'; +import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils'; + +export const dynamic = 'force-static'; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<RouteLayoutParams> } +) { + const { context } = await getStaticSiteContext(await params); + return serveLLMsFullTxt(context); +} diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 17f03b9318..925521e02b 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -431,6 +431,38 @@ const testCases: TestsCase[] = [ }, ], }, + { + name: 'llms.txt', + skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel', + contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/', + tests: [ + { + name: 'llms.txt', + url: 'llms.txt', + screenshot: false, + run: async (_page, response) => { + expect(response?.status()).toBe(200); + expect(response?.headers()['content-type']).toContain('text/markdown'); + }, + }, + ], + }, + { + name: 'llms-full.txt', + skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel', + contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/', + tests: [ + { + name: 'llms-full.txt', + url: 'llms-full.txt', + screenshot: false, + run: async (_page, response) => { + expect(response?.status()).toBe(200); + expect(response?.headers()['content-type']).toContain('text/markdown'); + }, + }, + ], + }, { name: 'Site subdirectory (proxy)', skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel', diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 8443bb615a..62952f844f 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -46,13 +46,20 @@ "katex": "^0.16.9", "mathjax": "^3.2.2", "mdast-util-to-markdown": "^2.1.2", + "mdast-util-from-markdown": "^2.0.2", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-gfm": "^3.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.0.0", "memoizee": "^0.4.17", "next": "14.2.26", "next-themes": "^0.2.1", "nuqs": "^2.2.3", "object-hash": "^3.0.0", "openapi-types": "^12.1.3", - "p-map": "^7.0.0", + "p-map": "^7.0.3", "parse-cache-control": "^1.0.1", "partial-json": "^0.1.7", "react": "^19.0.0", diff --git a/packages/gitbook/src/lib/urls.ts b/packages/gitbook/src/lib/urls.ts index 0bfa89e769..05dba4f3d0 100644 --- a/packages/gitbook/src/lib/urls.ts +++ b/packages/gitbook/src/lib/urls.ts @@ -8,3 +8,17 @@ export function checkIsHttpURL(input: string | URL): boolean { const parsed = new URL(input); return parsed.protocol === 'http:' || parsed.protocol === 'https:'; } + +/** + * True for absolute URLs (`scheme:*`) or hash-only anchors. + */ +export function checkIsExternalURL(input: string): boolean { + return URL.canParse(input); +} + +/** + * True for a hash-only anchor. + */ +export function checkIsAnchor(input: string): boolean { + return input.startsWith('#'); +} diff --git a/packages/gitbook/src/routes/llms-full.ts b/packages/gitbook/src/routes/llms-full.ts new file mode 100644 index 0000000000..90efb73f63 --- /dev/null +++ b/packages/gitbook/src/routes/llms-full.ts @@ -0,0 +1,209 @@ +import path from 'node:path'; +import { joinPath } from '@/lib/paths'; +import { getIndexablePages } from '@/lib/sitemap'; +import { getSiteStructureSections } from '@/lib/sites'; +import { checkIsAnchor, checkIsExternalURL } from '@/lib/urls'; +import type { RevisionPageDocument, SiteSection, SiteSpace } from '@gitbook/api'; +import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context'; +import { throwIfDataError } from '@v2/lib/data'; +import assertNever from 'assert-never'; +import type { Link, Paragraph, Root } from 'mdast'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'; +import { gfmFromMarkdown, gfmToMarkdown } from 'mdast-util-gfm'; +import { toMarkdown } from 'mdast-util-to-markdown'; +import { frontmatter } from 'micromark-extension-frontmatter'; +import { gfm } from 'micromark-extension-gfm'; +import { pMapIterable } from 'p-map'; +import { remove } from 'unist-util-remove'; +import { visit } from 'unist-util-visit'; + +// We limit the concurrency to 100 to avoid reaching limit with concurrent requests +// or file descriptor limits. +const MAX_CONCURRENCY = 100; + +/** + * Generate a llms-full.txt file for the site. + * As the result can be large, we stream it as we generate it. + */ +export async function serveLLMsFullTxt(context: GitBookSiteContext) { + if (!checkIsRootSiteContext(context)) { + return new Response('llms.txt is only served from the root of the site', { status: 404 }); + } + + return new Response( + new ReadableStream<Uint8Array>({ + async pull(controller) { + await streamMarkdownFromSiteStructure(context, controller); + controller.close(); + }, + }), + { + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + }, + } + ); +} + +/** + * Stream markdown from site structure. + */ +async function streamMarkdownFromSiteStructure( + context: GitBookSiteContext, + stream: ReadableStreamDefaultController<Uint8Array> +): Promise<void> { + switch (context.structure.type) { + case 'sections': + return streamMarkdownFromSections( + context, + stream, + getSiteStructureSections(context.structure, { ignoreGroups: true }) + ); + case 'siteSpaces': + return streamMarkdownFromSiteSpaces(context, stream, context.structure.structure, ''); + default: + assertNever(context.structure); + } +} + +/** + * Stream markdown from site sections. + */ +async function streamMarkdownFromSections( + context: GitBookSiteContext, + stream: ReadableStreamDefaultController<Uint8Array>, + siteSections: SiteSection[] +): Promise<void> { + for (const siteSection of siteSections) { + await streamMarkdownFromSiteSpaces( + context, + stream, + siteSection.siteSpaces, + siteSection.path + ); + } +} + +/** + * Stream markdown from site spaces. + */ +async function streamMarkdownFromSiteSpaces( + context: GitBookSiteContext, + stream: ReadableStreamDefaultController<Uint8Array>, + siteSpaces: SiteSpace[], + basePath: string +): Promise<void> { + const { dataFetcher } = context; + + for (const siteSpace of siteSpaces) { + const siteSpaceUrl = siteSpace.urls.published; + if (!siteSpaceUrl) { + continue; + } + const rootPages = await throwIfDataError( + dataFetcher.getRevisionPages({ + spaceId: siteSpace.space.id, + revisionId: siteSpace.space.revision, + metadata: false, + }) + ); + const pages = getIndexablePages(rootPages); + + for await (const markdown of pMapIterable( + pages, + async ({ page }) => { + if (page.type !== 'document') { + return ''; + } + + return getMarkdownForPage( + context, + siteSpace, + page, + joinPath(basePath, siteSpace.path) + ); + }, + { + concurrency: MAX_CONCURRENCY, + } + )) { + stream.enqueue(new TextEncoder().encode(markdown)); + } + } +} + +/** + * Get markdown from a page. + */ +async function getMarkdownForPage( + context: GitBookSiteContext, + siteSpace: SiteSpace, + page: RevisionPageDocument, + basePath: string +): Promise<string> { + const { dataFetcher } = context; + + const pageMarkdown = await throwIfDataError( + dataFetcher.getRevisionPageMarkdown({ + spaceId: siteSpace.space.id, + revisionId: siteSpace.space.revision, + pageId: page.id, + }) + ); + + const tree = fromMarkdown(pageMarkdown, { + extensions: [frontmatter(['yaml']), gfm()], + mdastExtensions: [frontmatterFromMarkdown(['yaml']), gfmFromMarkdown()], + }); + + // Remove frontmatter + remove(tree, 'yaml'); + + if (page.description) { + // The first node is the page title as a H1, we insert the description as a paragraph + // after it. + const descriptionNode: Paragraph = { + type: 'paragraph', + children: [{ type: 'text', value: page.description }], + }; + tree.children.splice(1, 0, descriptionNode); + } + + // Rewrite relative links to absolute links + transformLinks(context, tree, { currentPagePath: page.path, basePath }); + + const markdown = toMarkdown(tree, { extensions: [gfmToMarkdown()] }); + return `${markdown}\n\n`; +} + +/** + * Re-writes the URL of every relative <a> link so it is expressed from the site-root. + */ +export function transformLinks( + context: GitBookSiteContext, + tree: Root, + options: { currentPagePath: string; basePath: string } +): Root { + const { linker } = context; + const { currentPagePath, basePath } = options; + const currentDir = path.posix.dirname(currentPagePath); + + visit(tree, 'link', (node: Link) => { + const original = node.url; + + // Skip anchors, mailto:, http(s):, protocol-like, or already-rooted paths + if (checkIsExternalURL(original) || checkIsAnchor(original) || original.startsWith('/')) { + return; + } + + // Resolve against the current page’s directory and strip any leading “/” + const pathInSite = path.posix + .normalize(path.posix.join(basePath, currentDir, original)) + .replace(/^\/+/, ''); + + node.url = linker.toPathInSite(pathInSite); + }); + + return tree; +} diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts index 24f9119505..a7e953cbad 100644 --- a/packages/gitbook/src/routes/llms.ts +++ b/packages/gitbook/src/routes/llms.ts @@ -46,7 +46,7 @@ export async function serveLLMsTxt( }), { headers: { - 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Type': 'text/markdown; charset=utf-8', }, } ); From fa12f9eeb6dda45df2efc864aee804e5c8d8acf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Sun, 8 Jun 2025 14:57:50 +0200 Subject: [PATCH 080/127] Page cover dark-mode specific (#3287) --- .changeset/forty-readers-mix.md | 5 +++++ bun.lock | 12 ++++++------ package.json | 2 +- packages/cache-tags/package.json | 2 +- packages/gitbook-v2/package.json | 2 +- packages/gitbook/e2e/internal.spec.ts | 11 +++++++++++ packages/gitbook/package.json | 2 +- .../gitbook/src/components/PageBody/PageCover.tsx | 7 +++++++ packages/react-contentkit/package.json | 2 +- 9 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 .changeset/forty-readers-mix.md diff --git a/.changeset/forty-readers-mix.md b/.changeset/forty-readers-mix.md new file mode 100644 index 0000000000..98bede85d1 --- /dev/null +++ b/.changeset/forty-readers-mix.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Support dark-mode specific page cover image diff --git a/bun.lock b/bun.lock index 5a2653c915..e36213e8a1 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -51,7 +51,7 @@ "name": "gitbook", "version": "0.12.0", "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -150,7 +150,7 @@ "name": "gitbook-v2", "version": "0.3.0", "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", @@ -209,7 +209,7 @@ "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -267,7 +267,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -632,7 +632,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.118.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5bpvXyGNsMOn1Ee7uzohDz/yOgxpVyLZMLu6THYwbG9UeY6BFsWISeqTw03COCX42rVLU5zFReDxRTb7lfZtCw=="], + "@gitbook/api": ["@gitbook/api@0.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index 8ee2b33518..b4a3acc96b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index 37e243bdb6..7109f569d1 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 6eaa3a37c3..a0ead700f2 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "private": true, "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.1.0", "@sindresorhus/fnv1a": "^3.1.0", diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 925521e02b..a6d87fbf82 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -5,6 +5,7 @@ import { CustomizationHeaderPreset, CustomizationIconsStyle, CustomizationSidebarListStyle, + CustomizationThemeMode, } from '@gitbook/api'; import { expect } from '@playwright/test'; import jwt from 'jsonwebtoken'; @@ -681,6 +682,16 @@ const testCases: TestsCase[] = [ url: 'page-options/page-with-cover', run: waitForCookiesDialog, }, + { + name: 'With cover for dark mode', + url: `page-options/page-with-dark-cover${getCustomizationURL({ + themes: { + default: CustomizationThemeMode.Dark, + toggeable: false, + }, + })}`, + run: waitForCookiesDialog, + }, { name: 'With hero cover', url: 'page-options/page-with-hero-cover', diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 62952f844f..fba4221a85 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" }, "dependencies": { - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index 29fb1a4923..67aabd2124 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -22,6 +22,7 @@ export async function PageCover(props: { }) { const { as, page, cover, context } = props; const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null; + const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null; return ( <div @@ -58,6 +59,12 @@ export async function PageCover(props: { height: defaultPageCover.height, }, }, + dark: resolvedDark + ? { + src: resolvedDark.href, + size: resolvedDark.file?.dimensions, + } + : null, }} resize={ // When using the default cover, we don't want to resize as it's a SVG diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index 16eab1f683..aa2c162ee6 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.118.0", + "@gitbook/api": "^0.119.0", "@gitbook/icons": "workspace:*" }, "peerDependencies": { From d5992f1a5c3402fa496742afa49624348a6a5f3c Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 9 Jun 2025 10:40:44 +0200 Subject: [PATCH 081/127] Bump OpenNext to latest (#3288) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- bun.lock | 8 +++++--- .../openNext/customWorkers/doWrangler.jsonc | 18 ++++++++++++++++++ packages/gitbook-v2/package.json | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index e36213e8a1..dfd8d2ce4f 100644 --- a/bun.lock +++ b/bun.lock @@ -152,7 +152,7 @@ "dependencies": { "@gitbook/api": "^0.119.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.1.0", + "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", @@ -798,9 +798,9 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -3666,6 +3666,8 @@ "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "@opennextjs/aws/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], diff --git a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc index f5d2f726b8..e239202bef 100644 --- a/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc +++ b/packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc @@ -22,6 +22,12 @@ "bucket_name": "gitbook-open-v2-cache-preview" } ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-preview" + } + ], "durable_objects": { "bindings": [ { @@ -61,6 +67,12 @@ "service": "gitbook-x-staging-tail" } ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-staging" + } + ], "durable_objects": { "bindings": [ { @@ -100,6 +112,12 @@ "service": "gitbook-x-prod-tail" } ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-production" + } + ], "durable_objects": { "bindings": [ { diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index a0ead700f2..48aabef556 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -5,7 +5,7 @@ "dependencies": { "@gitbook/api": "^0.119.0", "@gitbook/cache-tags": "workspace:*", - "@opennextjs/cloudflare": "1.1.0", + "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", From f58b904ba6585d44f42f70d93ab72c70b7de6447 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Mon, 9 Jun 2025 11:37:29 +0200 Subject: [PATCH 082/127] Encode customization header (#3289) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/lazy-pants-matter.md | 6 ++++++ packages/gitbook-v2/src/middleware.ts | 3 ++- packages/gitbook/src/lib/customization.ts | 5 ++++- packages/gitbook/src/middleware.ts | 4 +++- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .changeset/lazy-pants-matter.md diff --git a/.changeset/lazy-pants-matter.md b/.changeset/lazy-pants-matter.md new file mode 100644 index 0000000000..6852b473b9 --- /dev/null +++ b/.changeset/lazy-pants-matter.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": patch +"gitbook": patch +--- + +encode customization header diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 916d53d61d..49ffb9dba8 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -226,7 +226,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { const customization = siteRequestURL.searchParams.get('customization'); if (customization && validateSerializedCustomization(customization)) { routeType = 'dynamic'; - requestHeaders.set(MiddlewareHeaders.Customization, customization); + // We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel. + requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization)); } const theme = siteRequestURL.searchParams.get('theme'); if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) { diff --git a/packages/gitbook/src/lib/customization.ts b/packages/gitbook/src/lib/customization.ts index 38440234b1..d23a0a88cb 100644 --- a/packages/gitbook/src/lib/customization.ts +++ b/packages/gitbook/src/lib/customization.ts @@ -12,9 +12,12 @@ export async function getDynamicCustomizationSettings( ): Promise<SiteCustomizationSettings> { const headersList = await headers(); const extend = headersList.get(MiddlewareHeaders.Customization); + if (extend) { try { - const parsedSettings = rison.decode_object<SiteCustomizationSettings>(extend); + // We need to decode it first as it is URL encoded, then decode the Rison object + const unencoded = decodeURIComponent(extend); + const parsedSettings = rison.decode_object<SiteCustomizationSettings>(unencoded); return parsedSettings; } catch (_error) {} diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 3e5f682ccc..9edb751243 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -201,7 +201,9 @@ export async function middleware(request: NextRequest) { const customization = url.searchParams.get('customization'); if (customization && validateSerializedCustomization(customization)) { - headers.set(MiddlewareHeaders.Customization, customization); + // We need to encode the customization to ensure it is properly encoded in vercel. + // We do it here as well so that we have a single method to decode it later. + headers.set(MiddlewareHeaders.Customization, encodeURIComponent(customization)); } const theme = url.searchParams.get('theme'); From c0ee60ec13c09efe7e96bff4f41d8001906e1317 Mon Sep 17 00:00:00 2001 From: Brett Jephson <brett@gitbook.io> Date: Mon, 9 Jun 2025 11:37:56 +0100 Subject: [PATCH 083/127] RND-6843: columns block (#3257) Co-authored-by: Steven Hall <steven@gitbook.io> --- .changeset/flat-wolves-poke.md | 6 ++ packages/gitbook/e2e/internal.spec.ts | 1 + .../src/components/DocumentView/Block.tsx | 12 +-- .../DocumentView/Columns/Columns.tsx | 80 +++++++++++++++++++ .../components/DocumentView/Columns/index.ts | 1 + 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 .changeset/flat-wolves-poke.md create mode 100644 packages/gitbook/src/components/DocumentView/Columns/Columns.tsx create mode 100644 packages/gitbook/src/components/DocumentView/Columns/index.ts diff --git a/.changeset/flat-wolves-poke.md b/.changeset/flat-wolves-poke.md new file mode 100644 index 0000000000..2b5c201cd1 --- /dev/null +++ b/.changeset/flat-wolves-poke.md @@ -0,0 +1,6 @@ +--- +"gitbook-v2": patch +"gitbook": patch +--- + +Adds Columns layout block to GBO diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index a6d87fbf82..3efe00bd27 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -666,6 +666,7 @@ const testCases: TestsCase[] = [ name: 'Stepper', url: 'blocks/stepper', }, + { name: 'Columns', url: 'blocks/columns' }, ], }, { diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index 3dba09bf56..ee4f9c7c8e 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -12,6 +12,7 @@ import type { ClassValue } from '@/lib/tailwind'; import { BlockContentRef } from './BlockContentRef'; import { CodeBlock } from './CodeBlock'; +import { Columns } from './Columns'; import { Divider } from './Divider'; import type { DocumentContextProps } from './DocumentView'; import { Drawing } from './Drawing'; @@ -68,6 +69,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) { return <List {...props} block={block} />; case 'list-item': return <ListItem {...props} block={block} />; + case 'columns': + return <Columns {...props} block={block} />; case 'code': return <CodeBlock {...props} block={block} />; case 'hint': @@ -112,10 +115,8 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) { case 'image': case 'code-line': case 'tabs-item': - throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); - case 'columns': case 'column': - return null; + throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); default: return nullIfNever(block); } @@ -171,6 +172,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue } case 'integration': case 'stepper': case 'reusable-content': + case 'columns': return <SkeletonCard id={id} style={style} />; case 'embed': case 'images': @@ -179,10 +181,8 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue } case 'image': case 'code-line': case 'tabs-item': - throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); - case 'columns': case 'column': - return null; + throw new Error(`Blocks (${block.type}) should be directly rendered by parent`); default: return nullIfNever(block); } diff --git a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx new file mode 100644 index 0000000000..4d74a0490d --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx @@ -0,0 +1,80 @@ +import { type ClassValue, tcls } from '@/lib/tailwind'; +import type { DocumentBlockColumns, Length } from '@gitbook/api'; +import type { BlockProps } from '../Block'; +import { Blocks } from '../Blocks'; + +export function Columns(props: BlockProps<DocumentBlockColumns>) { + const { block, style, ancestorBlocks, document, context } = props; + return ( + <div className={tcls('flex flex-col gap-x-8 md:flex-row', style)}> + {block.nodes.map((columnBlock) => { + const width = columnBlock.data.width; + const { className, style } = transformLengthToCSS(width); + return ( + <Column key={columnBlock.key} className={className} style={style}> + <Blocks + key={columnBlock.key} + nodes={columnBlock.nodes} + document={document} + ancestorBlocks={[...ancestorBlocks, block, columnBlock]} + context={context} + blockStyle="flip-heading-hash" + style="w-full space-y-4" + /> + </Column> + ); + })} + </div> + ); +} + +export function Column(props: { + children?: React.ReactNode; + className?: ClassValue; + style?: React.CSSProperties; +}) { + return ( + <div className={tcls('flex-col', props.className)} style={props.style}> + {props.children} + </div> + ); +} + +function transformLengthToCSS(length: Length | undefined) { + if (!length) { + return { className: ['md:w-full'] }; // default to full width if no length is specified + } + + if (typeof length === 'number') { + return { style: undefined }; // not implemented yet with non-percentage lengths + } + + if (length.unit === '%') { + return { + className: [ + 'md:flex-shrink-0', + COLUMN_WIDTHS[Math.round(length.value * 0.01 * (COLUMN_WIDTHS.length - 1))], + ], + }; + } + + return { style: undefined }; // not implemented yet with non-percentage lengths +} + +// Tailwind CSS classes for column widths. +// The index of the array corresponds to the percentage width of the column. +const COLUMN_WIDTHS = [ + 'md:w-0', + 'md:w-1/12', + 'md:w-2/12', + 'md:w-3/12', + 'md:w-4/12', + 'md:w-5/12', + 'md:w-6/12', + 'md:w-7/12', + 'md:w-8/12', + 'md:w-9/12', + 'md:w-10/12', + 'md:w-11/12', + 'md:w-full', +]; diff --git a/packages/gitbook/src/components/DocumentView/Columns/index.ts b/packages/gitbook/src/components/DocumentView/Columns/index.ts new file mode 100644 index 0000000000..a8b4f25b41 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Columns/index.ts @@ -0,0 +1 @@ +export * from './Columns'; From 231167d3e1ec3da9863d634051247045919f6cb8 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 10 Jun 2025 17:48:06 +0200 Subject: [PATCH 084/127] Make icons for page groups more contrasting (#3286) --- .changeset/sharp-jeans-burn.md | 5 +++++ .../gitbook/src/components/TableOfContents/TOCPageIcon.tsx | 2 +- .../src/components/TableOfContents/ToggleableLinkItem.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/sharp-jeans-burn.md diff --git a/.changeset/sharp-jeans-burn.md b/.changeset/sharp-jeans-burn.md new file mode 100644 index 0000000000..c990f8b7bf --- /dev/null +++ b/.changeset/sharp-jeans-burn.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Make icons for page groups more contrasting diff --git a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx index aef502c446..266f3c1e2b 100644 --- a/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx +++ b/packages/gitbook/src/components/TableOfContents/TOCPageIcon.tsx @@ -13,7 +13,7 @@ export function TOCPageIcon({ page }: { page: RevisionPage }) { page={page} style={tcls( 'text-base', - 'text-tint-strong/6', + '[.toclink_&]:text-tint-strong/6', 'group-aria-current-page/toclink:text-primary-subtle', 'contrast-more:group-aria-current-page/toclink:text-primary', diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 92502433ba..b118501f4c 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -67,7 +67,7 @@ function LinkItem( insights={insights} aria-current={isActive ? 'page' : undefined} className={tcls( - 'group/toclink relative transition-colors', + 'group/toclink toclink relative transition-colors', 'flex flex-row justify-between', 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', From 2860beacdc2562d7a359524b80fd9bb21f9edade Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Tue, 10 Jun 2025 19:05:07 +0200 Subject: [PATCH 085/127] revert to use withoutConcurrentExecution (#3292) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- packages/gitbook-v2/src/lib/data/api.ts | 1027 +++++++++-------- packages/gitbook-v2/src/lib/data/memoize.ts | 92 ++ packages/gitbook/e2e/customers.spec.ts | 10 +- .../components/DocumentView/InlineLink.tsx | 3 +- packages/gitbook/src/lib/references.tsx | 2 +- 5 files changed, 652 insertions(+), 482 deletions(-) create mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 44313bfc57..52ba17c1a3 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -9,6 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { DataFetcherError, wrapDataFetcherError } from './errors'; +import { withCacheKey, withoutConcurrentExecution } from './memoize'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -194,518 +195,594 @@ export function createDataFetcher( }; } -const getUserById = async (input: DataFetcherInput, params: { userId: string }) => { - 'use cache'; - return trace(`getUserById(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.users.getUserById(params.userId, { - ...noCacheFetchOptions, +const getUserById = withCacheKey( + withoutConcurrentExecution(async (_, input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, + }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; }); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; }); - }); -}; + }) +); + +const getSpace = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; shareKey: string | undefined } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); -const getSpace = async ( - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); + } + ) +); + +const getChangeRequest = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; changeRequestId: string } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); - return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getSpaceById( - params.spaceId, - { - shareKey: params.shareKey, - }, - { - ...noCacheFetchOptions, + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - }); -}; - -const getChangeRequest = async ( - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }) - ); - - return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId, - { - ...noCacheFetchOptions, + } + ) +); + +const getRevision = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); + } + ) +); + +const getRevisionPages = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); + } + ) +); + +const getRevisionFile = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; fileId: string } + ) => { + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - return res.data; - }); - }); -}; - -const getRevision = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - }, - { - ...noCacheFetchOptions, + } + ) +); + +const getRevisionPageMarkdown = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + format: 'markdown', + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); + } + return res.data.markdown; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - }); -}; - -const getRevisionPages = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } -) => { - 'use cache'; - return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - }, - { - ...noCacheFetchOptions, + } + ) +); + +const getRevisionPageByPath = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; path: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data.pages; - }); - }); -}; - -const getRevisionFile = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string } -) => { - 'use cache'; - return trace( - `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; + } + ) +); + +const getDocument = withCacheKey( + withoutConcurrentExecution( + async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); }); } - ); -}; - -const getRevisionPageMarkdown = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } -) => { - 'use cache'; - return trace( - `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, + ) +); + +const getComputedDocument = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { + spaceId: string; + organizationId: string; + source: ComputedContentSource; + seed: string; + } + ) => { + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( { - format: 'markdown', + spaceId: params.spaceId, + organizationId: params.organizationId, }, - { - ...noCacheFetchOptions, - } - ); - - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); + params.source + ) + ); - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getComputedDocument( + params.spaceId, + { + source: params.source, + seed: params.seed, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - return res.data.markdown; - }); - } - ); -}; - -const getRevisionPageByPath = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string } -) => { - 'use cache'; - return trace( - `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + ); } - ); -}; - -const getDocument = async ( - input: DataFetcherInput, - params: { spaceId: string; documentId: string } -) => { - 'use cache'; - return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getDocumentById( - params.spaceId, - params.documentId, - {}, - { - ...noCacheFetchOptions, + ) +); + +const getReusableContent = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; reusableContentId: string } + ) => { + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - }); -}; - -const getComputedDocument = async ( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - } -) => { - 'use cache'; - cacheTag( - ...getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ) - ); - - return trace( - `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getComputedDocument( - params.spaceId, - { - source: params.source, - seed: params.seed, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); } - ); -}; + ) +); + +const getLatestOpenAPISpecVersionContent = withCacheKey( + withoutConcurrentExecution( + async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'openapi', + organization: params.organizationId, + openAPISpec: params.slug, + }) + ); -const getReusableContent = async ( - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string } -) => { - 'use cache'; - return trace( - `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } + ); } - ); -}; - -const getLatestOpenAPISpecVersionContent = async ( - input: DataFetcherInput, - params: { organizationId: string; slug: string } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }) - ); + ) +); + +const getPublishedContentSite = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { organizationId: string; siteId: string; siteShareKey: string | undefined } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - return trace( - `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + } + ); } - ); -}; - -const getPublishedContentSite = async ( - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + ) +); + +const getSiteRedirectBySource = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; + } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - return trace( - `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + } + ); } - ); -}; - -const getSiteRedirectBySource = async ( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + ) +); + +const getEmbedByUrl = withCacheKey( + withoutConcurrentExecution( + async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); - return trace( - `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); }); } - ); -}; - -const getEmbedByUrl = async (input: DataFetcherInput, params: { spaceId: string; url: string }) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); + ) +); + +const searchSiteContent = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace( - params.spaceId, - { - url: params.url, - }, - { - ...noCacheFetchOptions, + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { + return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; + const api = apiClient(input); + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('hours'); + return res.data.items; + }); } ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('weeks'); - return res.data; - }); - }); -}; - -const searchSiteContent = async ( - input: DataFetcherInput, - params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); + } + ) +); + +const renderIntegrationUi = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { integrationName: string; request: RenderIntegrationUI } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) + ); - return trace( - `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, scope } = params; - const api = apiClient(input); - const res = await api.orgs.searchSiteContent( - organizationId, - siteId, - { - query, - ...scope, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('hours'); - return res.data.items; + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); }); } - ); -}; - -const renderIntegrationUi = async ( - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI } -) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'integration', - integration: params.integrationName, - }) - ); - - return trace(`renderIntegrationUi(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - }); -}; + ) +); async function* streamAIResponse( input: DataFetcherInput, diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts new file mode 100644 index 0000000000..5110d015fa --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/memoize.ts @@ -0,0 +1,92 @@ +import { cache } from 'react'; + +// This is used to create a context specific to the current request. +// This version works both in cloudflare and in vercel. +const getRequestContext = cache(() => ({})); + +/** + * Wrap a function by preventing concurrent executions of the same function. + * With a logic to work per-request in Cloudflare Workers. + */ +export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>( + wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType> +): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> { + const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>(); + + return (key: string, ...args: ArgsType) => { + const globalContext = getRequestContext(); + + /** + * Cache storage that is scoped to the current request when executed in Cloudflare Workers, + * to avoid "Cannot perform I/O on behalf of a different request" errors. + */ + const promiseCache = + globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>(); + globalPromiseCache.set(globalContext, promiseCache); + + const concurrent = promiseCache.get(key); + if (concurrent) { + return concurrent; + } + + const promise = (async () => { + try { + const result = await wrapped(key, ...args); + return result; + } finally { + promiseCache.delete(key); + } + })(); + + promiseCache.set(key, promise); + + return promise; + }; +} + +/** + * Wrap a function by passing it a cache key that is computed from the function arguments. + */ +export function withCacheKey<ArgsType extends any[], ReturnType>( + wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> +): (...args: ArgsType) => Promise<ReturnType> { + return (...args: ArgsType) => { + const cacheKey = getCacheKey(args); + return wrapped(cacheKey, ...args); + }; +} + +/** + * Compute a cache key from the function arguments. + */ +function getCacheKey(args: any[]) { + return JSON.stringify(deepSortValue(args)); +} + +function deepSortValue(value: unknown): unknown { + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' || + value === null || + value === undefined + ) { + return value; + } + + if (Array.isArray(value)) { + return value.map(deepSortValue); + } + + if (value && typeof value === 'object') { + return Object.entries(value) + .map(([key, subValue]) => { + return [key, deepSortValue(subValue)] as const; + }) + .sort((a, b) => { + return a[0].localeCompare(b[0]); + }); + } + + return value; +} diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts index f0612ba8e0..aa8e6e4a7c 100644 --- a/packages/gitbook/e2e/customers.spec.ts +++ b/packages/gitbook/e2e/customers.spec.ts @@ -204,11 +204,11 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://sosovalue-white-paper.gitbook.io', tests: [{ name: 'Home', url: '/' }], }, - { - name: 'docs.revrobotics.com', - contentBaseURL: 'https://docs.revrobotics.com', - tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], - }, + // { + // name: 'docs.revrobotics.com', + // contentBaseURL: 'https://docs.revrobotics.com', + // tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], + // }, { name: 'chartschool.stockcharts.com', contentBaseURL: 'https://chartschool.stockcharts.com', diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx index 37a812bb3c..f5bd7f798b 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink.tsx @@ -12,7 +12,8 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) { const resolved = context.contentContext ? await resolveContentRef(inline.data.ref, context.contentContext, { - resolveAnchorText: true, + // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link. + resolveAnchorText: false, }) : null; diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 7248ab2ded..8cfef861d0 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -147,7 +147,7 @@ export async function resolveContentRef( // Compute the text to display for the link if (anchor) { - text = `#${anchor}`; + text = page.title; ancestors.push({ label: page.title, icon: <PageIcon page={page} style={iconStyle} />, From e7a591d9f6bae7b157d06e96ae8b891937c2fb71 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Tue, 10 Jun 2025 19:25:29 +0200 Subject: [PATCH 086/127] Fix border being added to cards (#3294) --- .changeset/tame-mangos-battle.md | 5 +++++ packages/gitbook/src/components/primitives/Link.tsx | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .changeset/tame-mangos-battle.md diff --git a/.changeset/tame-mangos-battle.md b/.changeset/tame-mangos-battle.md new file mode 100644 index 0000000000..844bf321a5 --- /dev/null +++ b/.changeset/tame-mangos-battle.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix border being added to cards diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx index cf9d2c24c4..ff9f5ebde1 100644 --- a/packages/gitbook/src/components/primitives/Link.tsx +++ b/packages/gitbook/src/components/primitives/Link.tsx @@ -106,10 +106,7 @@ export const LinkOverlay = React.forwardRef(function LinkOverlay( <Link ref={ref} {...domProps} - className={tcls( - 'link-overlay static before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full', - className - )} + className={tcls('link-overlay absolute inset-0 z-10', className)} > {children} </Link> From fa3d2aae08ab1fd4073601358440c7fa253f54a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Wed, 11 Jun 2025 12:27:24 +0200 Subject: [PATCH 087/127] Fix inline expression crashing pages (#3297) --- .github/workflows/deploy-preview.yaml | 8 ++--- bun.lock | 12 +++---- package.json | 2 +- packages/cache-tags/package.json | 2 +- packages/gitbook-v2/package.json | 2 +- packages/gitbook/package.json | 2 +- .../src/components/DocumentView/Block.tsx | 8 +---- .../src/components/DocumentView/Inline.tsx | 31 +++++-------------- packages/gitbook/src/lib/typescript.ts | 7 +++++ packages/react-contentkit/package.json | 2 +- 10 files changed, 31 insertions(+), 45 deletions(-) diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index 7d8eec6109..c445004c0a 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-latest name: Visual Testing v1 needs: deploy-v1-cloudflare - timeout-minutes: 8 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 @@ -220,7 +220,7 @@ jobs: runs-on: ubuntu-latest name: Visual Testing Customers v1 needs: deploy-v1-cloudflare - timeout-minutes: 8 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 @@ -240,7 +240,7 @@ jobs: runs-on: ubuntu-latest name: Visual Testing Customers v2 needs: deploy-v2-vercel - timeout-minutes: 8 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 @@ -261,7 +261,7 @@ jobs: runs-on: ubuntu-latest name: Visual Testing Customers v2 (Cloudflare) needs: deploy-v2-cloudflare - timeout-minutes: 8 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/bun.lock b/bun.lock index dfd8d2ce4f..e3d406e11d 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -51,7 +51,7 @@ "name": "gitbook", "version": "0.12.0", "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -150,7 +150,7 @@ "name": "gitbook-v2", "version": "0.3.0", "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", @@ -209,7 +209,7 @@ "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -267,7 +267,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, @@ -632,7 +632,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.119.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-l2gfJ5+1HT3Sj1OQCh72lUOGU9LV9WbDjgKPjxIFz+NgOhSest1bJ3JhmHP66Xd4ETpmm0YQqnq4y18wKRMjBQ=="], + "@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index b4a3acc96b..dbb94b88ed 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index 7109f569d1..eafeabc44c 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 48aabef556..3993e746d3 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "private": true, "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index fba4221a85..74ef66a34e 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,7 +16,7 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" }, "dependencies": { - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index ee4f9c7c8e..c9f4ef493a 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -10,6 +10,7 @@ import { } from '@/components/primitives'; import type { ClassValue } from '@/lib/tailwind'; +import { nullIfNever } from '@/lib/typescript'; import { BlockContentRef } from './BlockContentRef'; import { CodeBlock } from './CodeBlock'; import { Columns } from './Columns'; @@ -45,13 +46,6 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext style?: ClassValue; } -/** - * Alternative to `assertNever` that returns `null` instead of throwing an error. - */ -function nullIfNever(_value: never): null { - return null; -} - export function Block<T extends DocumentBlock>(props: BlockProps<T>) { const { block, style, isEstimatedOffscreen, context } = props; diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx index 9101ffef20..1e1b091cef 100644 --- a/packages/gitbook/src/components/DocumentView/Inline.tsx +++ b/packages/gitbook/src/components/DocumentView/Inline.tsx @@ -1,16 +1,6 @@ -import type { - DocumentInline, - DocumentInlineAnnotation, - DocumentInlineButton, - DocumentInlineEmoji, - DocumentInlineImage, - DocumentInlineLink, - DocumentInlineMath, - DocumentInlineMention, - JSONDocument, -} from '@gitbook/api'; -import assertNever from 'assert-never'; +import type { DocumentInline, JSONDocument } from '@gitbook/api'; +import { nullIfNever } from '@/lib/typescript'; import { Annotation } from './Annotation/Annotation'; import type { DocumentContextProps } from './DocumentView'; import { Emoji } from './Emoji'; @@ -39,16 +29,7 @@ export interface InlineProps<T extends DocumentInline> extends DocumentContextPr children?: React.ReactNode; } -export function Inline< - T extends - | DocumentInlineImage - | DocumentInlineAnnotation - | DocumentInlineEmoji - | DocumentInlineLink - | DocumentInlineMath - | DocumentInlineMention - | DocumentInlineButton, ->(props: InlineProps<T>) { +export function Inline<T extends DocumentInline>(props: InlineProps<T>) { const { inline, ...contextProps } = props; switch (inline.type) { @@ -66,7 +47,11 @@ export function Inline< return <InlineImage {...contextProps} inline={inline} />; case 'button': return <InlineButton {...contextProps} inline={inline} />; + case 'expression': + // The GitBook API should take care of evaluating expressions. + // We should never need to render them. + return null; default: - assertNever(inline); + return nullIfNever(inline); } } diff --git a/packages/gitbook/src/lib/typescript.ts b/packages/gitbook/src/lib/typescript.ts index 43940d4a2a..f13574a5a4 100644 --- a/packages/gitbook/src/lib/typescript.ts +++ b/packages/gitbook/src/lib/typescript.ts @@ -4,3 +4,10 @@ export function filterOutNullable<T>(value: T): value is NonNullable<T> { return !!value; } + +/** + * Alternative to `assertNever` that returns `null` instead of throwing an error. + */ +export function nullIfNever(_value: never): null { + return null; +} diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index aa2c162ee6..eee7aa85b6 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.119.0", + "@gitbook/api": "^0.120.0", "@gitbook/icons": "workspace:*" }, "peerDependencies": { From 664ae8bff6ea1c2c391a370f6d934289fae26b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Wed, 11 Jun 2025 13:40:59 +0200 Subject: [PATCH 088/127] Make heading from stepper visible in outline (#3301) --- packages/gitbook/src/lib/document-sections.ts | 128 ++++++++++++------ 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts index 38619a5881..9e59122365 100644 --- a/packages/gitbook/src/lib/document-sections.ts +++ b/packages/gitbook/src/lib/document-sections.ts @@ -1,4 +1,4 @@ -import type { JSONDocument } from '@gitbook/api'; +import type { DocumentBlock, JSONDocument } from '@gitbook/api'; import type { GitBookAnyContext } from '@v2/lib/context'; import { getNodeText } from './document'; @@ -19,58 +19,102 @@ export interface DocumentSection { export async function getDocumentSections( context: GitBookAnyContext, document: JSONDocument +): Promise<DocumentSection[]> { + return getSectionsFromNodes(document.nodes, context); +} + +/** + * Extract a list of sections from a list of nodes. + */ +async function getSectionsFromNodes( + nodes: DocumentBlock[], + context: GitBookAnyContext ): Promise<DocumentSection[]> { const sections: DocumentSection[] = []; let depth = 0; - for (const block of document.nodes) { - if ((block.type === 'heading-1' || block.type === 'heading-2') && block.meta?.id) { - if (block.type === 'heading-1') { + for (const block of nodes) { + switch (block.type) { + case 'heading-1': { + const id = block.meta?.id; + if (!id) { + continue; + } depth = 1; - } - const title = getNodeText(block); - const id = block.meta.id; - - sections.push({ - id, - title, - depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1, - }); - } - - if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) { - const { data: operation } = await resolveOpenAPIOperationBlock({ - block, - context, - }); - if (operation) { + const title = getNodeText(block); sections.push({ - id: block.meta.id, - tag: operation.method.toUpperCase(), - title: operation.operation.summary || operation.path, + id, + title, depth: 1, - deprecated: operation.operation.deprecated, }); + continue; } - } - - if ( - block.type === 'openapi-schemas' && - !block.data.grouped && - block.meta?.id && - block.data.schemas.length === 1 - ) { - const { data } = await resolveOpenAPISchemasBlock({ - block, - context, - }); - const schema = data?.schemas[0]; - if (schema) { + case 'heading-2': { + const id = block.meta?.id; + if (!id) { + continue; + } + const title = getNodeText(block); sections.push({ - id: block.meta.id, - title: `The ${schema.name} object`, - depth: 1, + id, + title, + depth: depth > 0 ? 2 : 1, + }); + continue; + } + case 'stepper': { + const stepNodes = await Promise.all( + block.nodes.map(async (step) => getSectionsFromNodes(step.nodes, context)) + ); + for (const stepSections of stepNodes) { + sections.push(...stepSections); + } + continue; + } + case 'swagger': + case 'openapi-operation': { + const id = block.meta?.id; + if (!id) { + continue; + } + const { data: operation } = await resolveOpenAPIOperationBlock({ + block, + context, + }); + if (operation) { + sections.push({ + id, + tag: operation.method.toUpperCase(), + title: operation.operation.summary || operation.path, + depth: 1, + deprecated: operation.operation.deprecated, + }); + } + continue; + } + case 'openapi-schemas': { + const id = block.meta?.id; + if (!id) { + continue; + } + if (block.data.grouped || block.data.schemas.length !== 1) { + // Skip grouped schemas, they are not sections + continue; + } + + const { data } = await resolveOpenAPISchemasBlock({ + block, + context, }); + const schema = data?.schemas[0]; + if (schema) { + sections.push({ + id, + title: `The ${schema.name} object`, + depth: 1, + }); + } + continue; } } } From fbfcca5dae76be67ac885b83d30cf358a012f148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Wed, 11 Jun 2025 13:41:26 +0200 Subject: [PATCH 089/127] Fix rendering of font in ogimage (#3299) --- .changeset/poor-dodos-lick.md | 5 + .changeset/warm-roses-sleep.md | 5 + bun.lock | 98 +++++++++++++-- package.json | 10 +- packages/cache-tags/package.json | 2 +- packages/fonts/.gitignore | 2 + packages/fonts/README.md | 3 + packages/fonts/bin/generate.ts | 91 ++++++++++++++ packages/fonts/package.json | 31 +++++ .../__snapshots__/getDefaultFont.test.ts.snap | 57 +++++++++ packages/fonts/src/fonts.ts | 5 + packages/fonts/src/getDefaultFont.test.ts | 119 ++++++++++++++++++ packages/fonts/src/getDefaultFont.ts | 112 +++++++++++++++++ packages/fonts/src/index.ts | 2 + packages/fonts/src/types.ts | 17 +++ packages/fonts/tsconfig.build.json | 4 + packages/fonts/tsconfig.json | 24 ++++ packages/fonts/turbo.json | 9 ++ packages/gitbook-v2/package.json | 2 +- packages/gitbook/package.json | 3 +- packages/gitbook/src/routes/ogimage.tsx | 85 +++++-------- packages/icons/package.json | 4 +- packages/icons/tsconfig.build.json | 4 + packages/react-contentkit/package.json | 2 +- 24 files changed, 624 insertions(+), 72 deletions(-) create mode 100644 .changeset/poor-dodos-lick.md create mode 100644 .changeset/warm-roses-sleep.md create mode 100644 packages/fonts/.gitignore create mode 100644 packages/fonts/README.md create mode 100644 packages/fonts/bin/generate.ts create mode 100644 packages/fonts/package.json create mode 100644 packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap create mode 100644 packages/fonts/src/fonts.ts create mode 100644 packages/fonts/src/getDefaultFont.test.ts create mode 100644 packages/fonts/src/getDefaultFont.ts create mode 100644 packages/fonts/src/index.ts create mode 100644 packages/fonts/src/types.ts create mode 100644 packages/fonts/tsconfig.build.json create mode 100644 packages/fonts/tsconfig.json create mode 100644 packages/fonts/turbo.json create mode 100644 packages/icons/tsconfig.build.json diff --git a/.changeset/poor-dodos-lick.md b/.changeset/poor-dodos-lick.md new file mode 100644 index 0000000000..ca02d7382b --- /dev/null +++ b/.changeset/poor-dodos-lick.md @@ -0,0 +1,5 @@ +--- +"@gitbook/fonts": minor +--- + +Initial version of the package diff --git a/.changeset/warm-roses-sleep.md b/.changeset/warm-roses-sleep.md new file mode 100644 index 0000000000..e14b06cdc2 --- /dev/null +++ b/.changeset/warm-roses-sleep.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix ogimage using incorrect Google Font depending on language. diff --git a/bun.lock b/bun.lock index e3d406e11d..3ff6d61c4c 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "assert-never": "^1.2.1", }, "devDependencies": { @@ -47,15 +47,27 @@ "emoji-assets": "^8.0.0", }, }, + "packages/fonts": { + "name": "@gitbook/fonts", + "version": "0.0.0", + "dependencies": { + "@gitbook/api": "catalog:", + }, + "devDependencies": { + "google-font-metadata": "^6.0.3", + "typescript": "^5.5.3", + }, + }, "packages/gitbook": { "name": "gitbook", "version": "0.12.0", "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", + "@gitbook/fonts": "workspace:*", "@gitbook/icons": "workspace:*", "@gitbook/openapi-parser": "workspace:*", "@gitbook/react-contentkit": "workspace:*", @@ -150,7 +162,7 @@ "name": "gitbook-v2", "version": "0.3.0", "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", @@ -209,7 +221,7 @@ "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -267,10 +279,12 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.120.0", "react": "^19.0.0", "react-dom": "^19.0.0", }, + "catalog": { + "@gitbook/api": "^0.120.0", + }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -604,6 +618,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], + "@evan/concurrency": ["@evan/concurrency@0.0.3", "", {}, "sha512-vjhkm2nrXoM39G4aP/U4CC5vFv/ZlMRSjbTII0N65J9R0EpgjdGswnHKS1KSjfGAp/9zKSNaojBgi0SxKnGapw=="], + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="], @@ -644,6 +660,8 @@ "@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="], + "@gitbook/fonts": ["@gitbook/fonts@workspace:packages/fonts"], + "@gitbook/icons": ["@gitbook/icons@workspace:packages/icons"], "@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"], @@ -798,6 +816,22 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="], + + "@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="], + + "@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="], + + "@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="], + + "@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="], + + "@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="], + + "@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="], + + "@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], + "@opennextjs/aws": ["@opennextjs/aws@3.6.5", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-wni+CWlRCyWfhNfekQBBPPkrDDnaGdZLN9hMybKI0wKOKTO+zhPOqR65Eh3V0pzWAi84Sureb5mdMuLwCxAAcw=="], "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.2.1", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.5", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.19.1" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-cOco+nHwlo/PLB1bThF8IIvaS8PgAT9MEI5ZttFO/qt6spgvr2lUaPkpjgSIQmI3sBIEG2cLUykvQ2nbbZEcVw=="], @@ -1470,6 +1504,8 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], "bignumber.js": ["bignumber.js@9.1.2", "", {}, "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="], @@ -1482,6 +1518,8 @@ "body-parser": ["body-parser@2.0.2", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "^3.0.0", "type-is": "~1.6.18" } }, "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="], "boxen": ["boxen@4.2.0", "", { "dependencies": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "cli-boxes": "^2.2.0", "string-width": "^4.1.0", "term-size": "^2.1.0", "type-fest": "^0.8.1", "widest-line": "^3.1.0" } }, "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ=="], @@ -1506,6 +1544,8 @@ "bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "cacheable": ["cacheable@1.8.9", "", { "dependencies": { "hookified": "^1.7.1", "keyv": "^5.3.1" } }, "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ=="], "cacheable-request": ["cacheable-request@6.1.0", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^3.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^4.1.0", "responselike": "^1.0.2" } }, "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg=="], @@ -1602,10 +1642,16 @@ "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="], + "css-select": ["css-select@5.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg=="], + "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], + "css-what": ["css-what@6.1.0", "", {}, "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "cva": ["cva@1.0.0-beta.2", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "typescript": ">= 4.5.5 < 6" }, "optionalPeers": ["typescript"] }, "sha512-dqcOFe247I5pKxfuzqfq3seLL5iMYsTgo40Uw7+pKZAntPgFtR7Tmy59P5IVIq/XgB0NQWoIvYDt9TwHkuK8Cg=="], @@ -1662,6 +1708,14 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], + + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], + + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], @@ -1694,7 +1748,7 @@ "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "env-cmd": ["env-cmd@10.1.0", "", { "dependencies": { "commander": "^4.0.0", "cross-spawn": "^7.0.0" }, "bin": { "env-cmd": "bin/env-cmd.js" } }, "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA=="], @@ -1800,6 +1854,8 @@ "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + "fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1904,6 +1960,8 @@ "google-auth-library": ["google-auth-library@5.10.1", "", { "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", "gaxios": "^2.1.0", "gcp-metadata": "^3.4.0", "gtoken": "^4.1.0", "jws": "^4.0.0", "lru-cache": "^5.0.0" } }, "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg=="], + "google-font-metadata": ["google-font-metadata@6.0.3", "", { "dependencies": { "@evan/concurrency": "^0.0.3", "@octokit/core": "^6.1.2", "cac": "^6.7.14", "consola": "^3.3.3", "deepmerge": "^4.3.1", "json-stringify-pretty-compact": "^4.0.0", "linkedom": "^0.18.6", "pathe": "^1.1.2", "picocolors": "^1.1.1", "playwright": "^1.49.1", "stylis": "^4.3.4", "zod": "^3.24.1" }, "bin": { "gfm": "dist/cli.mjs" } }, "sha512-62+QB+nmBKppHrxwvILZkV/EvKctEAdzkKBIJY26TtwZkUjeEqmAQnE5uHvtMTdB8odqGpQu5LAKPqqUYHI9wA=="], + "google-p12-pem": ["google-p12-pem@2.0.5", "", { "dependencies": { "node-forge": "^0.10.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" } }, "sha512-7RLkxwSsMsYh9wQ5Vb2zRtkAHvqPvfoMGag+nugl1noYO7gf0844Yr9TIFA5NEBMAeVt2Z+Imu7CQMp3oNatzQ=="], "googleapis": ["googleapis@47.0.0", "", { "dependencies": { "google-auth-library": "^5.6.1", "googleapis-common": "^3.2.0" } }, "sha512-+Fnjgcc3Na/rk57dwxqW1V0HJXJFjnt3aqFlckULqAqsPkmex/AyJJe6MSlXHC37ZmlXEb9ZtPGUp5ZzRDXpHg=="], @@ -1980,12 +2038,16 @@ "hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + "html-tags": ["html-tags@3.3.1", "", {}, "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="], + "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], + "http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="], "http-errors": ["http-errors@1.4.0", "", { "dependencies": { "inherits": "2.0.1", "statuses": ">= 1.2.1 < 2" } }, "sha512-oLjPqve1tuOl5aRhv8GK5eHpqP1C9fb+Ol+XTLjKfLltE44zdDbEdjPSbU7Ch5rSNsVFqZn97SrMmZLdu1/YMw=="], @@ -2096,6 +2158,8 @@ "json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="], + "json-stringify-pretty-compact": ["json-stringify-pretty-compact@4.0.0", "", {}, "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="], + "json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="], "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], @@ -2134,6 +2198,8 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "linkedom": ["linkedom@0.18.11", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" } }, "sha512-K03GU3FUlnhBAP0jPb7tN7YJl7LbjZx30Z8h6wgLXusnKF7+BEZvfEbdkN/lO9LfFzxN3S0ZAriDuJ/13dIsLA=="], + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="], @@ -2364,6 +2430,8 @@ "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "nuqs": ["nuqs@2.2.3", "", { "dependencies": { "mitt": "^3.0.1" }, "peerDependencies": { "@remix-run/react": ">=2", "next": ">=14.2.0", "react": ">=18.2.0 || ^19.0.0-0", "react-router-dom": ">=6" }, "optionalPeers": ["@remix-run/react", "next", "react-router-dom"] }, "sha512-nMCcUW06KSqEXA0xp+LiRqDpIE59BVYbjZLe0HUisJAlswfihHYSsAjYTzV0lcE1thfh8uh+LqUHGdQ8qq8rfA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2454,7 +2522,7 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="], @@ -2722,6 +2790,8 @@ "stylelint": ["stylelint@16.16.0", "", { "dependencies": { "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "@csstools/media-query-list-parser": "^4.0.2", "@csstools/selector-specificity": "^5.0.0", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", "debug": "^4.3.7", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^10.0.7", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", "ignore": "^7.0.3", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.3", "postcss-resolve-nested-selector": "^0.1.6", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "resolve-from": "^5.0.0", "string-width": "^4.2.3", "supports-hyperlinks": "^3.2.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^5.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-40X5UOb/0CEFnZVEHyN260HlSSUxPES+arrUphOumGWgXERHfwCD0kNBVILgQSij8iliYVwlc0V7M5bcLP9vPg=="], + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -2826,6 +2896,8 @@ "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], + "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], + "uid-promise": ["uid-promise@1.0.0", "", {}, "sha512-R8375j0qwXyIu/7R0tjdF06/sElHqbmdmWC9M2qQHpEVbvE4I5+38KJI7LUUmQMp7NVq4tKHiBMkT0NFM453Ig=="], "undici": ["undici@5.28.4", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g=="], @@ -2852,6 +2924,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -4014,6 +4088,8 @@ "@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="], + "@vue/compiler-core/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "@vue/compiler-sfc/postcss": ["postcss@8.4.47", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.0", "source-map-js": "^1.2.1" } }, "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ=="], "@vueuse/integrations/@vueuse/core": ["@vueuse/core@11.2.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "11.2.0", "@vueuse/shared": "11.2.0", "vue-demi": ">=0.14.10" } }, "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA=="], @@ -4062,6 +4138,8 @@ "decamelize-keys/map-obj": ["map-obj@1.0.1", "", {}, "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "edge-runtime/async-listen": ["async-listen@3.0.1", "", {}, "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA=="], "edge-runtime/picocolors": ["picocolors@1.0.0", "", {}, "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="], @@ -4110,6 +4188,8 @@ "google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "google-font-metadata/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], "got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], @@ -4178,6 +4258,8 @@ "package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "parse5/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "path-match/path-to-regexp": ["path-to-regexp@1.9.0", "", { "dependencies": { "isarray": "0.0.1" } }, "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g=="], "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], @@ -4270,6 +4352,8 @@ "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], + "unenv/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "update-notifier/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], "url-parse-lax/prepend-http": ["prepend-http@2.0.0", "", {}, "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="], diff --git a/package.json b/package.json index dbb94b88ed..1faa750ba8 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,9 @@ "turbo": "^2.5.0", "vercel": "^39.3.0" }, - "packageManager": "bun@1.2.11", + "packageManager": "bun@1.2.15", "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "^0.120.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -34,7 +33,12 @@ "download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local", "clean": "turbo run clean" }, - "workspaces": ["packages/*"], + "workspaces": { + "packages": ["packages/*"], + "catalog": { + "@gitbook/api": "^0.120.0" + } + }, "patchedDependencies": { "decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch", "@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch" diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index eafeabc44c..a57048f2f6 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.3.1", "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/fonts/.gitignore b/packages/fonts/.gitignore new file mode 100644 index 0000000000..253839862f --- /dev/null +++ b/packages/fonts/.gitignore @@ -0,0 +1,2 @@ +dist/ +src/data/*.json diff --git a/packages/fonts/README.md b/packages/fonts/README.md new file mode 100644 index 0000000000..fef253f27f --- /dev/null +++ b/packages/fonts/README.md @@ -0,0 +1,3 @@ +# `@gitbook/fonts` + +Utilities to lookup default fonts supported by GitBook. diff --git a/packages/fonts/bin/generate.ts b/packages/fonts/bin/generate.ts new file mode 100644 index 0000000000..d4326d615c --- /dev/null +++ b/packages/fonts/bin/generate.ts @@ -0,0 +1,91 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import { APIv2 } from 'google-font-metadata'; + +import { CustomizationDefaultFont } from '@gitbook/api'; + +import type { FontDefinitions } from '../src/types'; + +const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = { + [CustomizationDefaultFont.Inter]: 'inter', + [CustomizationDefaultFont.FiraSans]: 'fira-sans-extra-condensed', + [CustomizationDefaultFont.IBMPlexSerif]: 'ibm-plex-serif', + [CustomizationDefaultFont.Lato]: 'lato', + [CustomizationDefaultFont.Merriweather]: 'merriweather', + [CustomizationDefaultFont.NotoSans]: 'noto-sans', + [CustomizationDefaultFont.OpenSans]: 'open-sans', + [CustomizationDefaultFont.Overpass]: 'overpass', + [CustomizationDefaultFont.Poppins]: 'poppins', + [CustomizationDefaultFont.Raleway]: 'raleway', + [CustomizationDefaultFont.Roboto]: 'roboto', + [CustomizationDefaultFont.RobotoSlab]: 'roboto-slab', + [CustomizationDefaultFont.SourceSansPro]: 'source-sans-3', + [CustomizationDefaultFont.Ubuntu]: 'ubuntu', + [CustomizationDefaultFont.ABCFavorit]: 'inter', +}; + +/** + * Scripts to generate the list of all icons. + */ +async function main() { + // @ts-expect-error - we build the object + const output: FontDefinitions = {}; + + for (const font of Object.values(CustomizationDefaultFont)) { + const googleFontName = googleFontsMap[font]; + const fontMetadata = APIv2[googleFontName.toLowerCase()]; + if (!fontMetadata) { + throw new Error(`Font ${googleFontName} not found`); + } + + output[font] = { + font: googleFontName, + unicodeRange: fontMetadata.unicodeRange, + variants: { + '400': {}, + '700': {}, + }, + }; + + Object.keys(output[font].variants).forEach((weight) => { + const variants = fontMetadata.variants[weight]; + const normalVariant = variants.normal; + if (!normalVariant) { + throw new Error(`Font ${googleFontName} has no normal variant`); + } + + output[font].variants[weight] = {}; + Object.entries(normalVariant).forEach(([script, url]) => { + output[font].variants[weight][script] = url.url.woff; + }); + }); + } + + await writeDataFile('fonts', JSON.stringify(output, null, 2)); +} + +/** + * We write both in dist and src as the build process might have happen already + * and tsc doesn't copy the files. + */ +async function writeDataFile(name, content) { + const srcData = path.resolve(__dirname, '../src/data'); + const distData = path.resolve(__dirname, '../dist/data'); + + // Ensure the directories exists + await Promise.all([ + fs.mkdir(srcData, { recursive: true }), + fs.mkdir(distData, { recursive: true }), + ]); + + await Promise.all([ + fs.writeFile(path.resolve(srcData, `${name}.json`), content), + fs.writeFile(path.resolve(distData, `${name}.json`), content), + ]); +} + +main().catch((error) => { + console.error(`Error generating icons list: ${error}`); + process.exit(1); +}); diff --git a/packages/fonts/package.json b/packages/fonts/package.json new file mode 100644 index 0000000000..dbfb2d7ca0 --- /dev/null +++ b/packages/fonts/package.json @@ -0,0 +1,31 @@ +{ + "name": "@gitbook/fonts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "development": "./src/index.ts", + "default": "./dist/index.js" + } + }, + "version": "0.0.0", + "dependencies": { + "@gitbook/api": "catalog:" + }, + "devDependencies": { + "google-font-metadata": "^6.0.3", + "typescript": "^5.5.3" + }, + "scripts": { + "generate": "bun ./bin/generate.js", + "build": "tsc --project tsconfig.build.json", + "typecheck": "tsc --noEmit", + "dev": "tsc -w", + "clean": "rm -rf ./dist && rm -rf ./src/data", + "unit": "bun test" + }, + "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"], + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap new file mode 100644 index 0000000000..89184be22c --- /dev/null +++ b/packages/fonts/src/__snapshots__/getDefaultFont.test.ts.snap @@ -0,0 +1,57 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`getDefaultFont should return correct object for Latin text 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff", +} +`; + +exports[`getDefaultFont should return correct object for Cyrillic text 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff", +} +`; + +exports[`getDefaultFont should return correct object for Greek text 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZxhjp-Ek-_0ewmM.woff", +} +`; + +exports[`getDefaultFont should handle mixed script text 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZthjp-Ek-_0ewmM.woff", +} +`; + +exports[`getDefaultFont should handle different font weights: regular 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff", +} +`; + +exports[`getDefaultFont should handle different font weights: bold 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuFuYAZ9hjp-Ek-_0ew.woff", +} +`; + +exports[`getDefaultFont should handle different fonts: inter 1`] = ` +{ + "font": "Inter", + "url": "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hjp-Ek-_0ew.woff", +} +`; + +exports[`getDefaultFont should handle different fonts: roboto 1`] = ` +{ + "font": "Roboto", + "url": "https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Mu4mxMKTU1Kg.woff", +} +`; diff --git a/packages/fonts/src/fonts.ts b/packages/fonts/src/fonts.ts new file mode 100644 index 0000000000..c9544efb19 --- /dev/null +++ b/packages/fonts/src/fonts.ts @@ -0,0 +1,5 @@ +import type { FontDefinitions } from './types'; + +import rawFonts from './data/fonts.json' with { type: 'json' }; + +export const fonts: FontDefinitions = rawFonts; diff --git a/packages/fonts/src/getDefaultFont.test.ts b/packages/fonts/src/getDefaultFont.test.ts new file mode 100644 index 0000000000..58afbeac93 --- /dev/null +++ b/packages/fonts/src/getDefaultFont.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from 'bun:test'; +import { CustomizationDefaultFont } from '@gitbook/api'; +import { getDefaultFont } from './getDefaultFont'; + +describe('getDefaultFont', () => { + it('should return null for invalid font', () => { + const result = getDefaultFont({ + font: 'invalid-font' as CustomizationDefaultFont, + text: 'Hello', + weight: 400, + }); + expect(result).toBeNull(); + }); + + it('should return null for invalid weight', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello', + weight: 999 as any, + }); + expect(result).toBeNull(); + }); + + it('should return null for text not supported by any script', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: '😀', // Emoji not supported by Inter + weight: 400, + }); + expect(result).toBeNull(); + }); + + it('should return correct object for Latin text', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello World', + weight: 400, + }); + expect(result).not.toBeNull(); + expect(result?.font).toBe(CustomizationDefaultFont.Inter); + expect(result).toMatchSnapshot(); + }); + + it('should return correct object for Cyrillic text', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Привет мир', + weight: 400, + }); + expect(result).not.toBeNull(); + expect(result?.font).toBe(CustomizationDefaultFont.Inter); + expect(result).toMatchSnapshot(); + }); + + it('should return correct object for Greek text', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Γεια σας', + weight: 400, + }); + expect(result).not.toBeNull(); + expect(result?.font).toBe(CustomizationDefaultFont.Inter); + expect(result).toMatchSnapshot(); + }); + + it('should handle mixed script text', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello Привет', + weight: 400, + }); + expect(result).not.toBeNull(); + expect(result?.font).toBe(CustomizationDefaultFont.Inter); + expect(result).toMatchSnapshot(); + }); + + it('should handle different font weights', () => { + const regular = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello', + weight: 400, + }); + const bold = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello', + weight: 700, + }); + expect(regular).not.toBeNull(); + expect(bold).not.toBeNull(); + expect(regular).toMatchSnapshot('regular'); + expect(bold).toMatchSnapshot('bold'); + }); + + it('should handle empty string', () => { + const result = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: '', + weight: 400, + }); + expect(result).toBeNull(); + }); + + it('should handle different fonts', () => { + const inter = getDefaultFont({ + font: CustomizationDefaultFont.Inter, + text: 'Hello', + weight: 400, + }); + const roboto = getDefaultFont({ + font: CustomizationDefaultFont.Roboto, + text: 'Hello', + weight: 400, + }); + expect(inter).not.toBeNull(); + expect(roboto).not.toBeNull(); + expect(inter).toMatchSnapshot('inter'); + expect(roboto).toMatchSnapshot('roboto'); + }); +}); diff --git a/packages/fonts/src/getDefaultFont.ts b/packages/fonts/src/getDefaultFont.ts new file mode 100644 index 0000000000..ea18e8bba2 --- /dev/null +++ b/packages/fonts/src/getDefaultFont.ts @@ -0,0 +1,112 @@ +import type { CustomizationDefaultFont } from '@gitbook/api'; +import { fonts } from './fonts'; +import type { FontWeight } from './types'; + +/** + * Get the URL to load a font for a text. + */ +export function getDefaultFont(input: { + /** + * GitBook font to use. + */ + font: CustomizationDefaultFont; + + /** + * Text to display with the font. + */ + text: string; + + /** + * Font weight to use. + */ + weight: FontWeight; +}): { font: string; url: string } | null { + if (!input.text.trim()) { + return null; + } + + const fontDefinition = fonts[input.font]; + if (!fontDefinition) { + return null; + } + + const variant = fontDefinition.variants[`${input.weight}`]; + if (!variant) { + return null; + } + + const script = getBestUnicodeRange(input.text, fontDefinition.unicodeRange); + if (!script) { + return null; + } + + return variant[script] + ? { + font: input.font, + url: variant[script], + } + : null; +} + +/** + * Determine which named @font-face unicode-range covers + * the greatest share of the characters in `text`. + * + * @param text The text you want to inspect. + * @param ranges An object whose keys are range names and whose + * values are CSS-style comma-separated unicode-range + * declarations (e.g. "U+0370-03FF,U+1F00-1FFF"). + * @returns The key of the best-matching range, or `null` + * when nothing matches at all. + */ +function getBestUnicodeRange(text: string, ranges: Record<string, string>): string | null { + // ---------- helper: parse "U+XXXX" or "U+XXXX-YYYY" ---------- + const parseOne = (token: string): [number, number] | null => { + token = token.trim().toUpperCase(); + if (!token.startsWith('U+')) return null; + + const body = token.slice(2); // drop "U+" + const [startHex, endHex] = body.split('-'); + const start = Number.parseInt(startHex, 16); + const end = endHex ? Number.parseInt(endHex, 16) : start; + + if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null; + return [start, end]; + }; + + // ---------- helper: build lookup table ---------- + const parsed: Record<string, [number, number][]> = {}; + for (const [label, list] of Object.entries(ranges)) { + parsed[label] = list + .split(',') + .map(parseOne) + .filter((x): x is [number, number] => x !== null); + } + + // ---------- tally code-point hits ---------- + const hits: Record<string, number> = Object.fromEntries(Object.keys(parsed).map((k) => [k, 0])); + + for (let i = 0; i < text.length; ) { + const cp = text.codePointAt(i)!; + i += cp > 0xffff ? 2 : 1; // advance by 1 UTF-16 code-unit (or 2 for surrogates) + + for (const [label, rangesArr] of Object.entries(parsed)) { + if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) { + hits[label]++; + } + } + } + + // ---------- choose the "best" ---------- + let winner: string | null = null; + let maxCount = 0; + + for (const [label, count] of Object.entries(hits)) { + if (count > maxCount) { + maxCount = count; + winner = label; + } + } + + return maxCount > 0 ? winner : null; +} diff --git a/packages/fonts/src/index.ts b/packages/fonts/src/index.ts new file mode 100644 index 0000000000..fbb3584899 --- /dev/null +++ b/packages/fonts/src/index.ts @@ -0,0 +1,2 @@ +export * from './getDefaultFont'; +export * from './types'; diff --git a/packages/fonts/src/types.ts b/packages/fonts/src/types.ts new file mode 100644 index 0000000000..899b32c49f --- /dev/null +++ b/packages/fonts/src/types.ts @@ -0,0 +1,17 @@ +import type { CustomizationDefaultFont } from '@gitbook/api'; + +export type FontWeight = 400 | 700; + +export type FontDefinition = { + font: string; + unicodeRange: { + [script: string]: string; + }; + variants: { + [weight in string]: { + [script: string]: string; + }; + }; +}; + +export type FontDefinitions = { [fontName in CustomizationDefaultFont]: FontDefinition }; diff --git a/packages/fonts/tsconfig.build.json b/packages/fonts/tsconfig.build.json new file mode 100644 index 0000000000..e4828bc1f6 --- /dev/null +++ b/packages/fonts/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "src/**/*.test.ts"] +} diff --git a/packages/fonts/tsconfig.json b/packages/fonts/tsconfig.json new file mode 100644 index 0000000000..2b3fe87c5f --- /dev/null +++ b/packages/fonts/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "ESNext", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowJs": true, + "noEmit": false, + "declaration": true, + "outDir": "dist", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "types": [ + "bun-types" // add Bun global + ] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/fonts/turbo.json b/packages/fonts/turbo.json new file mode 100644 index 0000000000..9097cda33c --- /dev/null +++ b/packages/fonts/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "generate": { + "inputs": ["bin/**/*", "package.json"], + "outputs": ["src/data/*.json", "dist/data/*.json"] + } + } +} diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 3993e746d3..5723cdbc79 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -3,7 +3,7 @@ "version": "0.3.0", "private": true, "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/cache-tags": "workspace:*", "@opennextjs/cloudflare": "1.2.1", "@sindresorhus/fnv1a": "^3.1.0", diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 74ef66a34e..666cd114d0 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -16,12 +16,13 @@ "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" }, "dependencies": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", "@gitbook/icons": "workspace:*", + "@gitbook/fonts": "workspace:*", "@gitbook/openapi-parser": "workspace:*", "@gitbook/react-contentkit": "workspace:*", "@gitbook/react-math": "workspace:*", diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index b086ffd1cd..e55dcfc2dd 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -1,5 +1,6 @@ import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api'; import { colorContrast } from '@gitbook/colors'; +import { type FontWeight, getDefaultFont } from '@gitbook/fonts'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; @@ -11,24 +12,6 @@ import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; import { getResizedImageURL } from '@v2/lib/images'; -const googleFontsMap: { [fontName in CustomizationDefaultFont]: string } = { - [CustomizationDefaultFont.Inter]: 'Inter', - [CustomizationDefaultFont.FiraSans]: 'Fira Sans Extra Condensed', - [CustomizationDefaultFont.IBMPlexSerif]: 'IBM Plex Serif', - [CustomizationDefaultFont.Lato]: 'Lato', - [CustomizationDefaultFont.Merriweather]: 'Merriweather', - [CustomizationDefaultFont.NotoSans]: 'Noto Sans', - [CustomizationDefaultFont.OpenSans]: 'Open Sans', - [CustomizationDefaultFont.Overpass]: 'Overpass', - [CustomizationDefaultFont.Poppins]: 'Poppins', - [CustomizationDefaultFont.Raleway]: 'Raleway', - [CustomizationDefaultFont.Roboto]: 'Roboto', - [CustomizationDefaultFont.RobotoSlab]: 'Roboto Slab', - [CustomizationDefaultFont.SourceSansPro]: 'Source Sans 3', - [CustomizationDefaultFont.Ubuntu]: 'Ubuntu', - [CustomizationDefaultFont.ABCFavorit]: 'Inter', -}; - /** * Render the OpenGraph image for a site content. */ @@ -65,19 +48,15 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page const { fontFamily, fonts } = await (async () => { // google fonts if (typeof customization.styling.font === 'string') { - const fontFamily = googleFontsMap[customization.styling.font] ?? 'Inter'; + const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter; const regularText = pageDescription; const boldText = `${contentTitle}${pageTitle}`; const fonts = ( await Promise.all([ - getWithCache(`google-font:${fontFamily}:400`, () => - loadGoogleFont({ fontFamily, text: regularText, weight: 400 }) - ), - getWithCache(`google-font:${fontFamily}:700`, () => - loadGoogleFont({ fontFamily, text: boldText, weight: 700 }) - ), + loadGoogleFont({ font: fontFamily, text: regularText, weight: 400 }), + loadGoogleFont({ font: fontFamily, text: boldText, weight: 700 }), ]) ).filter(filterOutNullable); @@ -260,37 +239,31 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page ); } -async function loadGoogleFont(input: { fontFamily: string; text: string; weight: 400 | 700 }) { - const { fontFamily, text, weight } = input; - - if (!text.trim()) { - return null; - } - - const url = new URL('https://fonts.googleapis.com/css2'); - url.searchParams.set('family', `${fontFamily}:wght@${weight}`); - url.searchParams.set('text', text); - - const result = await fetch(url.href); - if (!result.ok) { - return null; - } - - const css = await result.text(); - const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/); - const resourceUrl = resource ? resource[1] : null; - - if (resourceUrl) { - const response = await fetch(resourceUrl); - if (response.ok) { - const data = await response.arrayBuffer(); - return { - name: fontFamily, - data, - style: 'normal' as const, - weight, - }; - } +async function loadGoogleFont(input: { + font: CustomizationDefaultFont; + text: string; + weight: FontWeight; +}) { + const lookup = getDefaultFont({ + font: input.font, + text: input.text, + weight: input.weight, + }); + + // If we found a font file, load it + if (lookup) { + return getWithCache(`google-font-files:${lookup.url}`, async () => { + const response = await fetch(lookup.url); + if (response.ok) { + const data = await response.arrayBuffer(); + return { + name: lookup.font, + data, + style: 'normal' as const, + weight: input.weight, + }; + } + }); } // If for some reason we can't load the font, we'll just use the default one diff --git a/packages/icons/package.json b/packages/icons/package.json index 472b7a7b3b..0e0b802123 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -32,7 +32,7 @@ }, "scripts": { "generate": "bun ./bin/gen-list.js", - "build": "tsc", + "build": "tsc --project tsconfig.build.json", "typecheck": "tsc --noEmit", "dev": "tsc -w", "clean": "rm -rf ./dist && rm -rf ./src/data", @@ -41,7 +41,7 @@ "bin": { "gitbook-icons": "./bin/gitbook-icons.js" }, - "files": ["dist", "src", "bin", "data", "README.md", "CHANGELOG.md"], + "files": ["dist", "src", "bin", "README.md", "CHANGELOG.md"], "engines": { "node": ">=20.0.0" } diff --git a/packages/icons/tsconfig.build.json b/packages/icons/tsconfig.build.json new file mode 100644 index 0000000000..e4828bc1f6 --- /dev/null +++ b/packages/icons/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "src/**/*.test.ts"] +} diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index eee7aa85b6..af2f568544 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "^0.120.0", + "@gitbook/api": "catalog:", "@gitbook/icons": "workspace:*" }, "peerDependencies": { From 2d64c78787f4f94f8d67275eb8db8748ce7801a7 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 11 Jun 2025 13:47:34 +0200 Subject: [PATCH 090/127] return null for 404 cached page in the incremental cache (#3296) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../gitbook-v2/openNext/incrementalCache.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts index 28d1d6b8a1..8e4ca98635 100644 --- a/packages/gitbook-v2/openNext/incrementalCache.ts +++ b/packages/gitbook-v2/openNext/incrementalCache.ts @@ -49,17 +49,20 @@ class GitbookIncrementalCache implements IncrementalCache { const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey)); if (localCacheEntry) { span.setAttribute('cacheHit', 'local'); - return localCacheEntry.json(); + const result = (await localCacheEntry.json()) as WithLastModified< + CacheValue<CacheType> + >; + return this.returnNullOn404(result); } const r2Object = await r2.get(cacheKey); if (!r2Object) return null; span.setAttribute('cacheHit', 'r2'); - return { + return this.returnNullOn404({ value: await r2Object.json(), lastModified: r2Object.uploaded.getTime(), - }; + }); } catch (e) { console.error('Failed to get from cache', e); return null; @@ -68,6 +71,18 @@ class GitbookIncrementalCache implements IncrementalCache { ); } + //TODO: This is a workaround to handle 404 responses in the cache. + // It should be handled by OpenNext cache interception directly. This should be removed once OpenNext cache interception is fixed. + returnNullOn404<CacheType extends CacheEntryType = 'cache'>( + cacheEntry: WithLastModified<CacheValue<CacheType>> | null + ): WithLastModified<CacheValue<CacheType>> | null { + if (!cacheEntry?.value) return null; + if ('meta' in cacheEntry.value && cacheEntry.value.meta?.status === 404) { + return null; + } + return cacheEntry; + } + async set<CacheType extends CacheEntryType = 'cache'>( key: string, value: CacheValue<CacheType>, From 4fb2a4a6d7c4da9583bf7d83a2b03c718d128dc5 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Wed, 11 Jun 2025 14:03:45 +0200 Subject: [PATCH 091/127] Rework full-width layout, add support for full-width page option (#3293) Co-authored-by: conico974 <nicodorseuil@yahoo.fr> --- .changeset/long-cameras-protect.md | 5 +++++ .../src/components/DocumentView/Blocks.tsx | 4 ++-- .../components/DocumentView/Columns/Columns.tsx | 2 +- .../src/components/DocumentView/Divider.tsx | 2 +- .../gitbook/src/components/Footer/Footer.tsx | 10 +++++++--- .../gitbook/src/components/Header/Header.tsx | 5 +---- .../src/components/PageAside/PageAside.tsx | 4 +--- .../gitbook/src/components/PageBody/PageBody.tsx | 16 +++++++--------- .../src/components/PageBody/PageCover.tsx | 14 ++++++++++---- .../components/PageBody/PageFooterNavigation.tsx | 2 +- .../src/components/PageBody/PageHeader.tsx | 9 ++++++++- .../components/PageBody/PreservePageLayout.tsx | 12 ++++++------ .../src/components/RootLayout/globals.css | 4 ---- .../src/components/SitePage/SitePageSkeleton.tsx | 2 +- .../src/components/SpaceLayout/SpaceLayout.tsx | 1 + .../TableOfContents/TableOfContents.tsx | 1 + packages/gitbook/src/components/layout.ts | 2 +- packages/gitbook/tailwind.config.ts | 10 +++++++++- 18 files changed, 63 insertions(+), 42 deletions(-) create mode 100644 .changeset/long-cameras-protect.md diff --git a/.changeset/long-cameras-protect.md b/.changeset/long-cameras-protect.md new file mode 100644 index 0000000000..358b9be33d --- /dev/null +++ b/.changeset/long-cameras-protect.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Rework full-width layout, add support for full-width page option diff --git a/packages/gitbook/src/components/DocumentView/Blocks.tsx b/packages/gitbook/src/components/DocumentView/Blocks.tsx index 903acd2955..4bbe605d87 100644 --- a/packages/gitbook/src/components/DocumentView/Blocks.tsx +++ b/packages/gitbook/src/components/DocumentView/Blocks.tsx @@ -70,8 +70,8 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl style={[ 'mx-auto w-full decoration-primary/6', node.data && 'fullWidth' in node.data && node.data.fullWidth - ? 'max-w-screen-xl' - : 'max-w-3xl', + ? 'max-w-screen-2xl' + : 'page-full-width:ml-0 max-w-3xl', blockStyle, ]} isEstimatedOffscreen={isOffscreen} diff --git a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx index 4d74a0490d..181095434e 100644 --- a/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx +++ b/packages/gitbook/src/components/DocumentView/Columns/Columns.tsx @@ -19,7 +19,7 @@ export function Columns(props: BlockProps<DocumentBlockColumns>) { ancestorBlocks={[...ancestorBlocks, block, columnBlock]} context={context} blockStyle="flip-heading-hash" - style="w-full space-y-4" + style="w-full space-y-4 *:max-w-full" /> </Column> ); diff --git a/packages/gitbook/src/components/DocumentView/Divider.tsx b/packages/gitbook/src/components/DocumentView/Divider.tsx index 8f18593e2f..f787d044b5 100644 --- a/packages/gitbook/src/components/DocumentView/Divider.tsx +++ b/packages/gitbook/src/components/DocumentView/Divider.tsx @@ -7,5 +7,5 @@ import type { BlockProps } from './Block'; export function Divider(props: BlockProps<DocumentBlockDivider>) { const { style } = props; - return <hr className={tcls(style, 'border-tint-subtle')} />; + return <hr className={tcls(style, 'page-full-width:max-w-full border-tint-subtle')} />; } diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx index 5723d67f3c..454ac0f421 100644 --- a/packages/gitbook/src/components/Footer/Footer.tsx +++ b/packages/gitbook/src/components/Footer/Footer.tsx @@ -36,12 +36,16 @@ export function Footer(props: { context: GitBookSiteContext }) { <div className={tcls(CONTAINER_STYLE, 'px-4', 'py-8', 'lg:py-12', 'mx-auto')}> <div className={tcls( - 'mx-auto grid max-w-3xl justify-between gap-12 lg:max-w-none', + 'lg:!max-w-none mx-auto grid max-w-3xl site-full-width:max-w-screen-2xl justify-between gap-12', 'grid-cols-[auto_auto]', 'lg:grid-cols-[18rem_minmax(auto,_48rem)_auto]', 'xl:grid-cols-[18rem_minmax(auto,_48rem)_14rem]', + 'site-full-width:lg:grid-cols-[18rem_minmax(auto,_80rem)_auto]', + 'site-full-width:xl:grid-cols-[18rem_minmax(auto,_80rem)_14rem]', 'page-no-toc:lg:grid-cols-[minmax(auto,_48rem)_auto]', - 'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]' + 'page-no-toc:xl:grid-cols-[14rem_minmax(auto,_48rem)_14rem]', + '[body:has(.site-full-width,.page-no-toc)_&]:lg:grid-cols-[minmax(auto,_90rem)_auto]', + '[body:has(.site-full-width,.page-no-toc)_&]:xl:grid-cols-[14rem_minmax(auto,_90rem)_14rem]' )} > { @@ -102,7 +106,7 @@ export function Footer(props: { context: GitBookSiteContext }) { 'col-span-2 page-has-toc:lg:col-span-1 page-has-toc:lg:col-start-2 page-no-toc:xl:col-span-1 page-no-toc:xl:col-start-2' )} > - <div className="mx-auto flex max-w-3xl flex-col gap-10 sm:flex-row sm:gap-6"> + <div className="mx-auto flex max-w-3xl site-full-width:max-w-screen-2xl flex-col gap-10 sm:flex-row sm:gap-6"> {partition(customization.footer.groups, FOOTER_COLUMNS).map( (column, columnIndex) => ( <div diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 35c2583c41..304e853139 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -104,11 +104,9 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo 'lg:basis-40', 'md:max-w-[40%]', 'lg:max-w-lg', - 'lg:ml-[max(calc((100%-18rem-48rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - margin (3rem) + 'lg:ml-[max(calc((100%-18rem-48rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) 'xl:ml-[max(calc((100%-18rem-48rem-14rem-3rem)/2),1.5rem)]', // container (100%) - sidebar (18rem) - content (48rem) - outline (14rem) - margin (3rem) 'page-no-toc:lg:ml-[max(calc((100%-18rem-48rem-18rem-3rem)/2),0rem)]', - 'page-full-width:lg:ml-[max(calc((100%-18rem-103rem-3rem)/2),1.5rem)]', - 'page-full-width:2xl:ml-[max(calc((100%-18rem-96rem-14rem+3rem)/2),1.5rem)]', 'md:mr-auto', 'order-last', 'md:order-[unset]', @@ -195,7 +193,6 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo <div className={tcls( CONTAINER_STYLE, - 'page-default-width:max-w-[unset]', 'grow', 'flex', 'items-end', diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index 06821d964c..d00af1bf2d 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -48,11 +48,9 @@ export function PageAside(props: { 'group/aside', 'hidden', 'xl:flex', - // 'page-no-toc:lg:flex', 'flex-col', 'basis-56', - // 'page-no-toc:basis-40', - // 'page-no-toc:xl:basis-56', + 'xl:ml-12', 'grow-0', 'shrink-0', 'break-anywhere', // To prevent long words in headings from breaking the layout diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 6d548dec95..801db03ce2 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -27,7 +27,9 @@ export function PageBody(props: { const { page, context, ancestors, document, withPageFeedback } = props; const { customization } = context; - const asFullWidth = document ? hasFullWidthBlock(document) : false; + const contentFullWidth = document ? hasFullWidthBlock(document) : false; + const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q'; + const asFullWidth = pageFullWidth || contentFullWidth; const language = getSpaceLanguage(customization); const updatedAt = page.updatedAt ?? page.createdAt; @@ -36,15 +38,11 @@ export function PageBody(props: { <main className={tcls( 'relative min-w-0 flex-1', - 'py-8 lg:px-12', + 'mx-auto max-w-screen-2xl py-8', // Allow words to break if they are too long. 'break-anywhere', - // When in api page mode without the aside, we align with the border of the main content - 'page-api-block:xl:max-2xl:pr-0', - // Max size to ensure one column in api is aligned with rest of content (2 x 3xl) + (gap-3 + 2) * px-12 - 'page-api-block:mx-auto page-api-block:max-w-screen-2xl', - // page.layout.tableOfContents ? null : 'xl:ml-56', - asFullWidth ? 'page-full-width' : 'page-default-width', + pageFullWidth ? 'page-full-width 2xl:px-8' : 'page-default-width', + asFullWidth ? 'site-full-width' : 'site-default-width', page.layout.tableOfContents ? 'page-has-toc' : 'page-no-toc' )} > @@ -81,7 +79,7 @@ export function PageBody(props: { <PageFooterNavigation context={context} page={page} /> ) : null} - <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong"> + <div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong"> {updatedAt ? ( <p className="mr-auto text-sm"> {t(language, 'page_last_modified', <DateRelative value={updatedAt} />)} diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index 67aabd2124..a4bb712618 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -34,14 +34,20 @@ export async function PageCover(props: { ? [ 'sm:-mx-6', 'md:-mx-8', - '-lg:mr-8', - 'lg:ml-0', + 'lg:-mr-8', + 'lg:-ml-12', !page.layout.tableOfContents && context.customization.header.preset !== 'none' - ? 'xl:-ml-64' + ? 'xl:-ml-[19rem]' : null, ] - : ['sm:mx-auto', 'max-w-3xl', 'sm:rounded-md', 'mb-8'] + : [ + 'sm:mx-auto', + 'max-w-3xl ', + 'page-full-width:max-w-screen-2xl', + 'sm:rounded-md', + 'mb-8', + ] )} > <Image diff --git a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx index b0190b6b18..a6be37d412 100644 --- a/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx +++ b/packages/gitbook/src/components/PageBody/PageFooterNavigation.tsx @@ -32,8 +32,8 @@ export async function PageFooterNavigation(props: { 'mt-6', 'gap-2', 'max-w-3xl', + 'page-full-width:max-w-screen-2xl', 'mx-auto', - 'page-api-block:ml-0', 'text-tint' )} > diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx index b8b62f945d..7c1ad9cbf7 100644 --- a/packages/gitbook/src/components/PageBody/PageHeader.tsx +++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx @@ -23,7 +23,14 @@ export async function PageHeader(props: { return ( <header - className={tcls('max-w-3xl', 'mx-auto', 'mb-6', 'space-y-3', 'page-api-block:ml-0')} + className={tcls( + 'max-w-3xl', + 'page-full-width:max-w-screen-2xl', + 'mx-auto', + 'mb-6', + 'space-y-3', + 'page-api-block:ml-0' + )} > {ancestors.length > 0 && ( <nav> diff --git a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx index 7094711fbb..06a801034c 100644 --- a/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx +++ b/packages/gitbook/src/components/PageBody/PreservePageLayout.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; /** * This component preserves the layout of the page while loading a new one. - * This approach is needed as page layout (full width block) is done using CSS (`body:has(.page-full-width)`), + * This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`), * which becomes false while transitioning between the 2 page states: * - * 1. Page 1 with full width block: `body:has(.page-full-width)` is true - * 2. Loading skeleton while transitioning to page 2: `body:has(.page-full-width)` is false - * 3. Page 2 with full width block: `body:has(.page-full-width)` is true + * 1. Page 1 with full width block: `body:has(.site-full-width)` is true + * 2. Loading skeleton while transitioning to page 2: `body:has(.site-full-width)` is false + * 3. Page 2 with full width block: `body:has(.site-full-width)` is true * * This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2). */ @@ -24,9 +24,9 @@ export function PreservePageLayout(props: { asFullWidth: boolean }) { } if (asFullWidth) { - header.classList.add('page-full-width'); + header.classList.add('site-full-width'); } else { - header.classList.remove('page-full-width'); + header.classList.remove('site-full-width'); } }, [asFullWidth]); diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index 2bb3e1a223..a46758c0ae 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -143,10 +143,6 @@ margin-right: 0; width: calc(100% - var(--scrollbar-width)); } - body:has(.page-full-width) .scroll-nojump { - margin-left: 0; - width: 100%; - } } .elevate-link { diff --git a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx index 37d97167c9..7b840afbb0 100644 --- a/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx +++ b/packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx @@ -19,7 +19,7 @@ export function SitePageSkeleton() { 'lg:items-start' )} > - <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'page-full-width:mx-0')}> + <div className={tcls('flex-1', 'max-w-3xl', 'mx-auto', 'site-full-width:mx-0')}> <SkeletonHeading style={tcls('mb-8')} /> <SkeletonParagraph style={tcls('mb-4')} /> </div> diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 6b9aefd929..cedc2eee89 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -73,6 +73,7 @@ export function SpaceLayout(props: { 'flex-col', 'lg:flex-row', CONTAINER_STYLE, + 'site-full-width:max-w-full', // Ensure the footer is display below the viewport even if the content is not enough withFooter && 'min-h-[calc(100vh-64px)]', diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 1fb8b59442..7ca7a2c0c0 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -35,6 +35,7 @@ export function TableOfContents(props: { 'relative', 'z-[1]', 'lg:sticky', + 'lg:mr-12', // Server-side static positioning 'lg:top-0', diff --git a/packages/gitbook/src/components/layout.ts b/packages/gitbook/src/components/layout.ts index 250d3366ab..6a9b39a982 100644 --- a/packages/gitbook/src/components/layout.ts +++ b/packages/gitbook/src/components/layout.ts @@ -14,7 +14,7 @@ export const CONTAINER_STYLE: ClassValue = [ 'md:px-8', 'max-w-screen-2xl', 'mx-auto', - 'page-full-width:max-w-full', + // 'site-full-width:max-w-full', ]; /** diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 7981c337b2..192ad39eb8 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -447,6 +447,13 @@ const config: Config = { }, }, opacity: opacity(), + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, }, plugins: [ plugin(({ addVariant }) => { @@ -514,8 +521,9 @@ const config: Config = { /** * Variant when the page contains a block that will be rendered in full-width mode. */ + addVariant('site-full-width', 'body:has(.site-full-width) &'); + addVariant('site-default-width', 'body:has(.site-default-width) &'); addVariant('page-full-width', 'body:has(.page-full-width) &'); - addVariant('page-default-width', 'body:has(.page-default-width) &'); /** * Variant when the page is configured to hide the table of content. From 902c3c6c1aef2a72040ea285716dfeb26f44f6de Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 11 Jun 2025 14:05:30 +0200 Subject: [PATCH 092/127] apply customization for dynamic site context in v2 (#3302) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/tidy-dots-suffer.md | 5 +++++ packages/gitbook-v2/src/app/utils.ts | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 .changeset/tidy-dots-suffer.md diff --git a/.changeset/tidy-dots-suffer.md b/.changeset/tidy-dots-suffer.md new file mode 100644 index 0000000000..c622927590 --- /dev/null +++ b/.changeset/tidy-dots-suffer.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +apply customization for dynamic context diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts index da810b3b90..932902df0e 100644 --- a/packages/gitbook-v2/src/app/utils.ts +++ b/packages/gitbook-v2/src/app/utils.ts @@ -1,4 +1,5 @@ import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive'; +import { getDynamicCustomizationSettings } from '@/lib/customization'; import type { SiteAPIToken } from '@gitbook/api'; import { type SiteURLData, fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context'; import { jwtDecode } from 'jwt-decode'; @@ -67,6 +68,8 @@ export async function getDynamicSiteContext(params: RouteLayoutParams) { siteURLData ); + context.customization = await getDynamicCustomizationSettings(context.customization); + return { context, visitorAuthClaims: getVisitorAuthClaims(siteURLData), From d41038195702f677882f3f2efb33eb1c77294b0c Mon Sep 17 00:00:00 2001 From: spastorelli <spastorelli@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:25:05 +0200 Subject: [PATCH 093/127] Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list (#3303) --- .changeset/clever-jokes-yell.md | 5 +++++ packages/gitbook-v2/src/middleware.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/clever-jokes-yell.md diff --git a/.changeset/clever-jokes-yell.md b/.changeset/clever-jokes-yell.md new file mode 100644 index 0000000000..87b2698636 --- /dev/null +++ b/.changeset/clever-jokes-yell.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Add docs.testgitbook.com to ADAPTIVE_CONTENT_HOSTS list diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 49ffb9dba8..c11d466c78 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -41,6 +41,8 @@ const ADAPTIVE_CONTENT_HOSTS = [ 'docs.gitbook.com', 'adaptive-docs.gitbook-staging.com', 'enriched-content-playground.gitbook-staging.io', + 'docs.testgitbook.com', + 'launchdarkly-site.gitbook.education', ]; export async function middleware(request: NextRequest) { From b6b597564dc5ebe174d38582ee275ad8aa14c1b9 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Wed, 11 Jun 2025 14:44:07 +0200 Subject: [PATCH 094/127] Reverse order of feedback smileys (#3300) --- .changeset/tiny-zoos-scream.md | 5 +++++ .../components/PageFeedback/PageFeedbackForm.tsx | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/tiny-zoos-scream.md diff --git a/.changeset/tiny-zoos-scream.md b/.changeset/tiny-zoos-scream.md new file mode 100644 index 0000000000..1c80e7684e --- /dev/null +++ b/.changeset/tiny-zoos-scream.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Reverse order of feedback smileys diff --git a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx index 551a8b83f5..2c15f52831 100644 --- a/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx +++ b/packages/gitbook/src/components/PageFeedback/PageFeedbackForm.tsx @@ -64,10 +64,10 @@ export function PageFeedbackForm(props: { <div className="rounded-full border border-tint-subtle bg-tint-base contrast-more:border-tint-12"> <div className="flex"> <RatingButton - rating={PageFeedbackRating.Bad} - label={tString(languages, 'was_this_helpful_negative')} - onClick={() => onSubmitRating(PageFeedbackRating.Bad)} - active={rating === PageFeedbackRating.Bad} + rating={PageFeedbackRating.Good} + label={tString(languages, 'was_this_helpful_positive')} + onClick={() => onSubmitRating(PageFeedbackRating.Good)} + active={rating === PageFeedbackRating.Good} disabled={rating !== undefined} /> <RatingButton @@ -78,10 +78,10 @@ export function PageFeedbackForm(props: { disabled={rating !== undefined} /> <RatingButton - rating={PageFeedbackRating.Good} - label={tString(languages, 'was_this_helpful_positive')} - onClick={() => onSubmitRating(PageFeedbackRating.Good)} - active={rating === PageFeedbackRating.Good} + rating={PageFeedbackRating.Bad} + label={tString(languages, 'was_this_helpful_negative')} + onClick={() => onSubmitRating(PageFeedbackRating.Bad)} + active={rating === PageFeedbackRating.Bad} disabled={rating !== undefined} /> </div> From 7a00880e5b57446ea3d99a8db355d9f5d34d21de Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 11 Jun 2025 15:34:52 +0200 Subject: [PATCH 095/127] Improve support for OAuth2 security type (#3304) --- .changeset/fifty-ducks-press.md | 6 + .../components/DocumentView/OpenAPI/style.css | 20 +++ .../react-openapi/src/OpenAPISchemaName.tsx | 14 ++- .../react-openapi/src/OpenAPISecurities.tsx | 118 ++++++++++++++++-- packages/react-openapi/src/translations/de.ts | 1 + packages/react-openapi/src/translations/en.ts | 1 + packages/react-openapi/src/translations/es.ts | 1 + packages/react-openapi/src/translations/fr.ts | 1 + packages/react-openapi/src/translations/ja.ts | 1 + packages/react-openapi/src/translations/nl.ts | 1 + packages/react-openapi/src/translations/no.ts | 1 + .../react-openapi/src/translations/pt-br.ts | 1 + packages/react-openapi/src/translations/zh.ts | 1 + 13 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 .changeset/fifty-ducks-press.md diff --git a/.changeset/fifty-ducks-press.md b/.changeset/fifty-ducks-press.md new file mode 100644 index 0000000000..a250d31243 --- /dev/null +++ b/.changeset/fifty-ducks-press.md @@ -0,0 +1,6 @@ +--- +'@gitbook/react-openapi': patch +'gitbook': patch +--- + +Improve support for OAuth2 security type diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index 1a88b4faa6..c855ec9fd8 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -310,6 +310,26 @@ @apply py-2 border-b border-tint-subtle max-w-full flex-1; } +.openapi-securities-oauth-flows { + @apply flex flex-col gap-2 divide-y divide-tint-subtle; +} + +.openapi-securities-oauth-content { + @apply prose *:!prose-sm *:text-tint; +} + +.openapi-securities-oauth-content.openapi-markdown code { + @apply text-xs; +} + +.openapi-securities-oauth-content ul { + @apply !my-0; +} + +.openapi-securities-url { + @apply ml-0.5 px-0.5 rounded hover:bg-tint transition-colors; +} + .openapi-securities-body { @apply flex flex-col gap-2; } diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx index d24efe3f70..8994b4d71f 100644 --- a/packages/react-openapi/src/OpenAPISchemaName.tsx +++ b/packages/react-openapi/src/OpenAPISchemaName.tsx @@ -27,12 +27,14 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { {propertyName} </span> ) : null} - <span> - {type ? <span className="openapi-schema-type">{type}</span> : null} - {additionalItems ? ( - <span className="openapi-schema-type">{additionalItems}</span> - ) : null} - </span> + {type || additionalItems ? ( + <span> + {type ? <span className="openapi-schema-type">{type}</span> : null} + {additionalItems ? ( + <span className="openapi-schema-type">{additionalItems}</span> + ) : null} + </span> + ) : null} {schema?.readOnly ? ( <span className="openapi-schema-readonly"> {t(context.translation, 'read_only')} diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx index 3f86236506..8b007ff3c1 100644 --- a/packages/react-openapi/src/OpenAPISecurities.tsx +++ b/packages/react-openapi/src/OpenAPISecurities.tsx @@ -1,5 +1,7 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { InteractiveSection } from './InteractiveSection'; import { Markdown } from './Markdown'; +import { OpenAPICopyButton } from './OpenAPICopyButton'; import { OpenAPISchemaName } from './OpenAPISchemaName'; import type { OpenAPIClientContext } from './context'; import { t } from './translate'; @@ -105,13 +107,7 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI /> ); case 'oauth2': - return ( - <OpenAPISchemaName - context={context} - propertyName="OAuth2" - required={security.required} - /> - ); + return <OpenAPISchemaOAuth2Flows context={context} security={security} />; case 'openIdConnect': return ( <OpenAPISchemaName @@ -125,3 +121,111 @@ function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPI return security.type; } } + +function OpenAPISchemaOAuth2Flows(props: { + context: OpenAPIClientContext; + security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean }; +}) { + const { context, security } = props; + + const flows = Object.entries(security.flows ?? {}); + + return ( + <div className="openapi-securities-oauth-flows"> + {flows.map(([name, flow], index) => ( + <OpenAPISchemaOAuth2Item + key={index} + flow={flow} + name={name} + context={context} + security={security} + /> + ))} + </div> + ); +} + +function OpenAPISchemaOAuth2Item(props: { + flow: NonNullable<OpenAPIV3.OAuth2SecurityScheme['flows']>[keyof NonNullable< + OpenAPIV3.OAuth2SecurityScheme['flows'] + >]; + name: string; + context: OpenAPIClientContext; + security: OpenAPIV3.OAuth2SecurityScheme & { required?: boolean }; +}) { + const { flow, context, security, name } = props; + + if (!flow) { + return null; + } + + const scopes = Object.entries(flow?.scopes ?? {}); + + return ( + <div> + <OpenAPISchemaName + context={context} + propertyName="OAuth2" + type={name} + required={security.required} + /> + <div className="openapi-securities-oauth-content openapi-markdown"> + {security.description ? <Markdown source={security.description} /> : null} + {'authorizationUrl' in flow && flow.authorizationUrl ? ( + <span> + Authorization URL:{' '} + <OpenAPICopyButton + value={flow.authorizationUrl} + context={context} + className="openapi-securities-url" + withTooltip + > + {flow.authorizationUrl} + </OpenAPICopyButton> + </span> + ) : null} + {'tokenUrl' in flow && flow.tokenUrl ? ( + <span> + Token URL:{' '} + <OpenAPICopyButton + value={flow.tokenUrl} + context={context} + className="openapi-securities-url" + withTooltip + > + {flow.tokenUrl} + </OpenAPICopyButton> + </span> + ) : null} + {'refreshUrl' in flow && flow.refreshUrl ? ( + <span> + Refresh URL:{' '} + <OpenAPICopyButton + value={flow.refreshUrl} + context={context} + className="openapi-securities-url" + withTooltip + > + {flow.refreshUrl} + </OpenAPICopyButton> + </span> + ) : null} + {scopes.length ? ( + <div> + {t(context.translation, 'available_scopes')}:{' '} + <ul> + {scopes.map(([key, value]) => ( + <li key={key}> + <OpenAPICopyButton value={key} context={context} withTooltip> + <code>{key}</code> + </OpenAPICopyButton> + : {value} + </li> + ))} + </ul> + </div> + ) : null} + </div> + </div> + ); +} diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts index 8a5236c587..6229acd4fa 100644 --- a/packages/react-openapi/src/translations/de.ts +++ b/packages/react-openapi/src/translations/de.ts @@ -35,6 +35,7 @@ export const de = { show: 'Zeige ${1}', hide: 'Verstecke ${1}', available_items: 'Verfügbare Elemente', + available_scopes: 'Verfügbare scopes', properties: 'Eigenschaften', or: 'oder', and: 'und', diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts index 4276595d54..3681626898 100644 --- a/packages/react-openapi/src/translations/en.ts +++ b/packages/react-openapi/src/translations/en.ts @@ -35,6 +35,7 @@ export const en = { show: 'Show ${1}', hide: 'Hide ${1}', available_items: 'Available items', + available_scopes: 'Available scopes', possible_values: 'Possible values', properties: 'Properties', or: 'or', diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts index f5ac905f6a..f9e8c58f48 100644 --- a/packages/react-openapi/src/translations/es.ts +++ b/packages/react-openapi/src/translations/es.ts @@ -35,6 +35,7 @@ export const es = { show: 'Mostrar ${1}', hide: 'Ocultar ${1}', available_items: 'Elementos disponibles', + available_scopes: 'Scopes disponibles', properties: 'Propiedades', or: 'o', and: 'y', diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts index b2e563c813..fde7a9222c 100644 --- a/packages/react-openapi/src/translations/fr.ts +++ b/packages/react-openapi/src/translations/fr.ts @@ -35,6 +35,7 @@ export const fr = { show: 'Afficher ${1}', hide: 'Masquer ${1}', available_items: 'Éléments disponibles', + available_scopes: 'Scopes disponibles', properties: 'Propriétés', or: 'ou', and: 'et', diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts index e393dd6cbd..04d43f67ae 100644 --- a/packages/react-openapi/src/translations/ja.ts +++ b/packages/react-openapi/src/translations/ja.ts @@ -35,6 +35,7 @@ export const ja = { show: '${1}を表示', hide: '${1}を非表示', available_items: '利用可能なアイテム', + available_scopes: '利用可能なスコープ', properties: 'プロパティ', or: 'または', and: 'かつ', diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts index 34867ccf15..2c57d7af4f 100644 --- a/packages/react-openapi/src/translations/nl.ts +++ b/packages/react-openapi/src/translations/nl.ts @@ -35,6 +35,7 @@ export const nl = { show: 'Toon ${1}', hide: 'Verberg ${1}', available_items: 'Beschikbare items', + available_scopes: 'Beschikbare scopes', properties: 'Eigenschappen', or: 'of', and: 'en', diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts index 2701177932..9ef1b80048 100644 --- a/packages/react-openapi/src/translations/no.ts +++ b/packages/react-openapi/src/translations/no.ts @@ -35,6 +35,7 @@ export const no = { show: 'Vis ${1}', hide: 'Skjul ${1}', available_items: 'Tilgjengelige elementer', + available_scopes: 'Tilgjengelige scopes', properties: 'Egenskaper', or: 'eller', and: 'og', diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts index 00e8ab3c27..2e9e7cb2d9 100644 --- a/packages/react-openapi/src/translations/pt-br.ts +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -35,6 +35,7 @@ export const pt_br = { show: 'Mostrar ${1}', hide: 'Ocultar ${1}', available_items: 'Itens disponíveis', + available_scopes: 'Scopes disponíveis', properties: 'Propriedades', or: 'ou', and: 'e', diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts index f8299d1993..f0e81f21bc 100644 --- a/packages/react-openapi/src/translations/zh.ts +++ b/packages/react-openapi/src/translations/zh.ts @@ -35,6 +35,7 @@ export const zh = { show: '显示${1}', hide: '隐藏${1}', available_items: '可用项', + available_scopes: '可用范围', properties: '属性', or: '或', and: '和', From c3b620e975a74ea7fa79d37dc24873a5828d601c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Thu, 12 Jun 2025 09:30:08 +0200 Subject: [PATCH 096/127] Preserve current variant when navigating between sections (#3306) --- .changeset/hip-bobcats-cover.md | 5 ++++ .../src/components/Header/SpacesDropdown.tsx | 19 ++---------- .../SiteSections/encodeClientSiteSections.ts | 29 +++++++++++++++--- packages/gitbook/src/lib/sites.ts | 30 +++++++++++++++++++ 4 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 .changeset/hip-bobcats-cover.md diff --git a/.changeset/hip-bobcats-cover.md b/.changeset/hip-bobcats-cover.md new file mode 100644 index 0000000000..9bd789f85d --- /dev/null +++ b/.changeset/hip-bobcats-cover.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Best effort at preserving current variant when navigating between sections by matching the pathname against site spaces in the new section. diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 1523774493..dd6887d955 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -1,8 +1,7 @@ import type { SiteSpace } from '@gitbook/api'; +import { getSiteSpaceURL } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; - -import { joinPath } from '@/lib/paths'; import type { GitBookSiteContext } from '@v2/lib/context'; import { DropdownChevron, DropdownMenu } from './DropdownMenu'; import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem'; @@ -14,7 +13,6 @@ export function SpacesDropdown(props: { className?: string; }) { const { context, siteSpace, siteSpaces, className } = props; - const { linker } = context; return ( <DropdownMenu @@ -73,9 +71,7 @@ export function SpacesDropdown(props: { variantSpace={{ id: otherSiteSpace.id, title: otherSiteSpace.title, - url: otherSiteSpace.urls.published - ? linker.toLinkForContent(otherSiteSpace.urls.published) - : getFallbackSiteSpaceURL(otherSiteSpace, context), + url: getSiteSpaceURL(context, otherSiteSpace), }} active={otherSiteSpace.id === siteSpace.id} /> @@ -83,14 +79,3 @@ export function SpacesDropdown(props: { </DropdownMenu> ); } - -/** - * When the site is not published yet, `urls.published` is not available. - * To ensure navigation works in preview, we compute a relative URL from the siteSpace path. - */ -function getFallbackSiteSpaceURL(siteSpace: SiteSpace, context: GitBookSiteContext) { - const { linker, sections } = context; - return linker.toPathInSite( - sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path - ); -} diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts index 49b73a8e1a..1c8cd09899 100644 --- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts +++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts @@ -1,3 +1,4 @@ +import { getSectionURL, getSiteSpaceURL } from '@/lib/sites'; import type { SiteSection, SiteSectionGroup } from '@gitbook/api'; import type { GitBookSiteContext, SiteSections } from '@v2/lib/context'; @@ -46,15 +47,35 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections: } function encodeSection(context: GitBookSiteContext, section: SiteSection) { - const { linker } = context; return { id: section.id, title: section.title, description: section.description, icon: section.icon, object: section.object, - url: section.urls.published - ? linker.toLinkForContent(section.urls.published) - : linker.toPathInSite(section.path), + url: findBestTargetURL(context, section), }; } + +/** + * Find the best default site space to navigate to for a givent section: + * 1. If we are on the default, continue on the default. + * 2. If a site space has the same path as the current one, return it. + * 3. Otherwise, return the default one. + */ +function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) { + const { siteSpace: currentSiteSpace } = context; + + if (section.siteSpaces.length === 1 || currentSiteSpace.default) { + return getSectionURL(context, section); + } + + const bestMatch = section.siteSpaces.find( + (siteSpace) => siteSpace.path === currentSiteSpace.path + ); + if (bestMatch) { + return getSiteSpaceURL(context, bestMatch); + } + + return getSectionURL(context, section); +} diff --git a/packages/gitbook/src/lib/sites.ts b/packages/gitbook/src/lib/sites.ts index 88d63e75d3..1827683e48 100644 --- a/packages/gitbook/src/lib/sites.ts +++ b/packages/gitbook/src/lib/sites.ts @@ -1,4 +1,6 @@ import type { SiteSection, SiteSectionGroup, SiteSpace, SiteStructure } from '@gitbook/api'; +import type { GitBookSiteContext } from '@v2/lib/context'; +import { joinPath } from './paths'; /** * Get all sections from a site structure. @@ -64,6 +66,34 @@ export function findSiteSpaceById(siteStructure: SiteStructure, spaceId: string) return null; } +/** + * Get the URL to navigate to for a section. + * When the site is not published yet, `urls.published` is not available. + * To ensure navigation works in preview, we compute a relative URL from the siteSection path. + */ +export function getSectionURL(context: GitBookSiteContext, section: SiteSection) { + const { linker } = context; + return section.urls.published + ? linker.toLinkForContent(section.urls.published) + : linker.toPathInSite(section.path); +} + +/** + * Get the URL to navigate to for a site space. + * When the site is not published yet, `urls.published` is not available. + * To ensure navigation works in preview, we compute a relative URL from the siteSpace path. + */ +export function getSiteSpaceURL(context: GitBookSiteContext, siteSpace: SiteSpace) { + const { linker, sections } = context; + if (siteSpace.urls.published) { + return linker.toLinkForContent(siteSpace.urls.published); + } + + return linker.toPathInSite( + sections?.current ? joinPath(sections.current.path, siteSpace.path) : siteSpace.path + ); +} + function findSiteSpaceByIdInSections(sections: SiteSection[], spaceId: string): SiteSpace | null { for (const section of sections) { const siteSpace = From 9316ccd1f4f0a52fe13849a2804a5f19dab67592 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:57:37 +0200 Subject: [PATCH 097/127] Update Models page styling (#3307) --- .changeset/fresh-shrimps-flow.md | 5 +++++ .../src/components/DocumentView/OpenAPI/style.css | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/fresh-shrimps-flow.md diff --git a/.changeset/fresh-shrimps-flow.md b/.changeset/fresh-shrimps-flow.md new file mode 100644 index 0000000000..a2e62c9480 --- /dev/null +++ b/.changeset/fresh-shrimps-flow.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Update Models page styling diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index c855ec9fd8..337f0b2ed2 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -6,7 +6,7 @@ } .openapi-schemas { - @apply flex flex-col mb-14 flex-1; + @apply flex flex-col mb-14 gap-0 flex-1; } .openapi-schemas-title { @@ -748,8 +748,12 @@ body:has(.openapi-select-popover) { } /* Disclosure */ +.openapi-schemas-disclosure { + @apply border-t border-x last:border-b border-tint-subtle !ring-0 first:!rounded-t-xl last:!rounded-b-xl !rounded-none; +} + .openapi-schemas-disclosure > .openapi-disclosure-trigger { - @apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1; + @apply flex items-center font-mono transition-all font-normal text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-5 truncate -outline-offset-1; } .openapi-schemas-disclosure > .openapi-disclosure-trigger, @@ -793,7 +797,6 @@ body:has(.openapi-select-popover) { @apply bg-tint-subtle overflow-hidden; } -.openapi-disclosure:has(> .openapi-disclosure-trigger:hover), .openapi-disclosure[data-expanded="true"] { @apply ring-1 shadow-sm; } From fecdbe8dde38cd23e14508df47a8b3a0520da3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Thu, 12 Jun 2025 12:25:28 +0200 Subject: [PATCH 098/127] Support dynamic expressions (#3295) --- packages/gitbook-v2/src/lib/data/api.ts | 45 +++++++++++++++++ packages/gitbook-v2/src/lib/data/index.ts | 2 +- packages/gitbook-v2/src/lib/data/pages.ts | 48 +++++++++++++++++-- packages/gitbook-v2/src/lib/data/types.ts | 9 ++++ packages/gitbook/package.json | 2 +- .../gitbook/src/components/PDF/PDFPage.tsx | 5 +- .../src/components/SitePage/SitePage.tsx | 2 +- packages/gitbook/src/lib/api.ts | 33 +++++++++++++ packages/gitbook/src/lib/references.tsx | 2 +- packages/gitbook/src/lib/v1.ts | 13 +++++ packages/gitbook/src/lib/waitUntil.ts | 10 ++++ packages/gitbook/tests/preload-bun.ts | 8 ++++ 12 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 packages/gitbook/tests/preload-bun.ts diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 52ba17c1a3..88c95c4016 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -113,6 +113,15 @@ export function createDataFetcher( }) ); }, + getRevisionPageDocument(params) { + return trace('getRevisionPageDocument', () => + getRevisionPageDocument(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + pageId: params.pageId, + }) + ); + }, getReusableContent(params) { return trace('getReusableContent', () => getReusableContent(input, { @@ -417,6 +426,42 @@ const getRevisionPageMarkdown = withCacheKey( ) ); +const getRevisionPageDocument = withCacheKey( + withoutConcurrentExecution( + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageDocumentInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + evaluated: true, + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + return res.data; + }); + } + ); + } + ) +); + const getRevisionPageByPath = withCacheKey( withoutConcurrentExecution( async ( diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts index 2e37e2fbb4..93266d124e 100644 --- a/packages/gitbook-v2/src/lib/data/index.ts +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -1,7 +1,7 @@ export * from './api'; export * from './types'; -export * from './pages'; export * from './urls'; export * from './errors'; export * from './lookup'; export * from './visitor'; +export * from './pages'; diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts index 4324254d9b..642b486b82 100644 --- a/packages/gitbook-v2/src/lib/data/pages.ts +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -1,15 +1,30 @@ -import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api'; +import { waitUntil } from '@/lib/waitUntil'; +import type { JSONDocument, RevisionPageDocument } from '@gitbook/api'; +import type { GitBookSiteContext, GitBookSpaceContext } from '../context'; import { getDataOrNull } from './errors'; -import type { GitBookDataFetcher } from './types'; /** * Get the document for a page. */ export async function getPageDocument( - dataFetcher: GitBookDataFetcher, - space: Space, + context: GitBookSpaceContext | GitBookSiteContext, page: RevisionPageDocument ): Promise<JSONDocument | null> { + const { dataFetcher, space } = context; + + if ( + 'site' in context && + (context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj') + ) { + return getDataOrNull( + dataFetcher.getRevisionPageDocument({ + spaceId: space.id, + revisionId: space.revision, + pageId: page.id, + }) + ); + } + if (page.documentId) { return getDataOrNull( dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId }) @@ -26,5 +41,30 @@ export async function getPageDocument( ); } + // Pre-fetch the document to start filling the cache before we migrate to this API. + if (isInPercentRollout(space.id, 10)) { + await waitUntil( + getDataOrNull( + dataFetcher.getRevisionPageDocument({ + spaceId: space.id, + revisionId: space.revision, + pageId: page.id, + }) + ) + ); + } + return null; } + +function isInPercentRollout(value: string, rollout: number) { + return getRandomPercent(value) < rollout; +} + +function getRandomPercent(value: string) { + const hash = value.split('').reduce((acc, char) => { + return acc + char.charCodeAt(0); + }, 0); + + return hash % 100; +} diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 178a0ba77d..8d987047e2 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -106,6 +106,15 @@ export interface GitBookDataFetcher { pageId: string; }): Promise<DataFetcherResponse<string>>; + /** + * Get the document of a page by its path. + */ + getRevisionPageDocument(params: { + spaceId: string; + revisionId: string; + pageId: string; + }): Promise<DataFetcherResponse<api.JSONDocument>>; + /** * Get a document by its space ID and document ID. */ diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 666cd114d0..379f975a1e 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -10,7 +10,7 @@ "typecheck": "tsc --noEmit", "e2e": "playwright test e2e/internal.spec.ts", "e2e-customers": "playwright test e2e/customers.spec.ts", - "unit": "bun test {src,packages}", + "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts", "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math", "copy:icons": "gitbook-icons ./public/~gitbook/static/icons", "clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math" diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index 96cb784ccd..f70b340b41 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -9,7 +9,6 @@ import { } from '@gitbook/api'; import { Icon } from '@gitbook/icons'; import type { GitBookSiteContext, GitBookSpaceContext } from '@v2/lib/context'; -import { getPageDocument } from '@v2/lib/data'; import type { GitBookLinker } from '@v2/lib/links'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; @@ -29,6 +28,7 @@ import { PageControlButtons } from './PageControlButtons'; import { PrintButton } from './PrintButton'; import './pdf.css'; import { sanitizeGitBookAppURL } from '@/lib/app'; +import { getPageDocument } from '@v2/lib/data'; const DEFAULT_LIMIT = 100; @@ -224,8 +224,7 @@ async function PDFPageDocument(props: { context: GitBookSpaceContext; }) { const { page, context } = props; - const { space } = context; - const document = await getPageDocument(context.dataFetcher, space, page); + const document = await getPageDocument(context, page); return ( <PrintPage id={getPagePDFContainerId(page)}> diff --git a/packages/gitbook/src/components/SitePage/SitePage.tsx b/packages/gitbook/src/components/SitePage/SitePage.tsx index fe6934a529..9e7c08515f 100644 --- a/packages/gitbook/src/components/SitePage/SitePage.tsx +++ b/packages/gitbook/src/components/SitePage/SitePage.tsx @@ -62,7 +62,7 @@ export async function SitePage(props: SitePageProps) { const withSections = Boolean(sections && sections.list.length > 0); const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader }; - const document = await getPageDocument(context.dataFetcher, context.space, page); + const document = await getPageDocument(context, page); return ( <PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}> diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 310074450d..cb612664b0 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -493,6 +493,39 @@ export const getRevisionPageByPath = cache({ }, }); +/** + * Get a document from a page by its ID + */ +export const getRevisionPageDocument = cache({ + name: 'api.getRevisionPageDocument.v1', + tag: (spaceId, revisionId) => + getCacheTag({ tag: 'revision', space: spaceId, revision: revisionId }), + tagImmutable: true, + getKeySuffix: getAPIContextId, + get: async ( + spaceId: string, + revisionId: string, + pageId: string, + options: CacheFunctionOptions + ) => { + const apiCtx = await api(); + const response = await apiCtx.client.spaces.getPageDocumentInRevisionById( + spaceId, + revisionId, + pageId, + { + evaluated: true, + }, + { + ...noCacheFetchOptions, + signal: options.signal, + } + ); + + return cacheResponse(response, cacheTtl_7days); + }, +}); + /** * Resolve a file by its ID. * It should not be used directly, use `getRevisionFile` instead. diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 8cfef861d0..7362decbe6 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -155,7 +155,7 @@ export async function resolveContentRef( }); if (resolveAnchorText) { - const document = await getPageDocument(dataFetcher, space, page); + const document = await getPageDocument(context, page); if (document) { const block = getBlockById(document, anchor); if (block) { diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index eb2accdc24..db6118a84c 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -25,6 +25,7 @@ import { getRevision, getRevisionFile, getRevisionPageByPath, + getRevisionPageDocument, getRevisionPages, getSiteRedirectBySource, getSpace, @@ -241,6 +242,18 @@ function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher { ); }, + getRevisionPageDocument(params) { + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevisionPageDocument( + params.spaceId, + params.revisionId, + params.pageId + ); + }) + ); + }, + getRevisionPageByPath(params) { return withAPI(() => wrapDataFetcherError(async () => { diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts index 6e2940f17b..e81e662e2d 100644 --- a/packages/gitbook/src/lib/waitUntil.ts +++ b/packages/gitbook/src/lib/waitUntil.ts @@ -1,4 +1,6 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; +import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare'; +import { isV2 } from './v2'; let pendings: Array<Promise<unknown>> = []; @@ -47,6 +49,14 @@ export async function waitUntil(promise: Promise<unknown>) { return; } + if (isV2()) { + const context = getCloudflareContextV2(); + if (context) { + context.ctx.waitUntil(promise); + return; + } + } + const cloudflareContext = await getGlobalContext(); if ('waitUntil' in cloudflareContext) { cloudflareContext.waitUntil(promise); diff --git a/packages/gitbook/tests/preload-bun.ts b/packages/gitbook/tests/preload-bun.ts new file mode 100644 index 0000000000..3fc846ae26 --- /dev/null +++ b/packages/gitbook/tests/preload-bun.ts @@ -0,0 +1,8 @@ +import { mock } from 'bun:test'; + +/** + * Mock the `server-only` module to avoid errors when running tests as it doesn't work well in Bun + */ +mock.module('server-only', () => { + return {}; +}); From da67711c90f93c281a7ca650fd78ac073d60db30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Thu, 12 Jun 2025 23:37:38 +0200 Subject: [PATCH 099/127] Fix crash on Vercel (#3311) --- packages/gitbook-v2/src/lib/data/pages.ts | 2 +- packages/gitbook/src/lib/waitUntil.ts | 33 ++++++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts index 642b486b82..f4cbb1acab 100644 --- a/packages/gitbook-v2/src/lib/data/pages.ts +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -42,7 +42,7 @@ export async function getPageDocument( } // Pre-fetch the document to start filling the cache before we migrate to this API. - if (isInPercentRollout(space.id, 10)) { + if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') { await waitUntil( getDataOrNull( dataFetcher.getRevisionPageDocument({ diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts index e81e662e2d..ed606e959d 100644 --- a/packages/gitbook/src/lib/waitUntil.ts +++ b/packages/gitbook/src/lib/waitUntil.ts @@ -1,5 +1,6 @@ import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types'; import { getCloudflareContext as getCloudflareContextV2 } from '@v2/lib/data/cloudflare'; +import { GITBOOK_RUNTIME } from '@v2/lib/env'; import { isV2 } from './v2'; let pendings: Array<Promise<unknown>> = []; @@ -49,20 +50,32 @@ export async function waitUntil(promise: Promise<unknown>) { return; } - if (isV2()) { - const context = getCloudflareContextV2(); - if (context) { - context.ctx.waitUntil(promise); - return; + if (GITBOOK_RUNTIME === 'cloudflare') { + if (isV2()) { + const context = getCloudflareContextV2(); + if (context) { + context.ctx.waitUntil(promise); + return; + } + } else { + const cloudflareContext = await getGlobalContext(); + if ('waitUntil' in cloudflareContext) { + cloudflareContext.waitUntil(promise); + return; + } } } - const cloudflareContext = await getGlobalContext(); - if ('waitUntil' in cloudflareContext) { - cloudflareContext.waitUntil(promise); - } else { - await promise; + if (GITBOOK_RUNTIME === 'vercel' && isV2()) { + // @ts-expect-error - `after` is not exported by `next/server` in next 14 + const { after } = await import('next/server'); + if (typeof after === 'function') { + after(() => promise); + return; + } } + + await promise; } /** From 315717fa2d219f3571eaf3a7bbbef418066d1da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 13 Jun 2025 11:17:33 +0200 Subject: [PATCH 100/127] Disable the background fetch of getRevisionPageDocument (#3313) --- packages/gitbook-v2/src/lib/data/pages.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts index f4cbb1acab..b9e3cabc5e 100644 --- a/packages/gitbook-v2/src/lib/data/pages.ts +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -42,16 +42,19 @@ export async function getPageDocument( } // Pre-fetch the document to start filling the cache before we migrate to this API. - if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') { - await waitUntil( - getDataOrNull( - dataFetcher.getRevisionPageDocument({ - spaceId: space.id, - revisionId: space.revision, - pageId: page.id, - }) - ) - ); + if (process.env.NODE_ENV === 'development') { + // Disable for now to investigate side-effects + if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') { + await waitUntil( + getDataOrNull( + dataFetcher.getRevisionPageDocument({ + spaceId: space.id, + revisionId: space.revision, + pageId: page.id, + }) + ) + ); + } } return null; From 50e8b2ec52e74dcb00d895c25b43bcf7971b855f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 13 Jun 2025 11:30:18 +0200 Subject: [PATCH 101/127] Don't use next/server after (#3314) --- packages/gitbook/src/lib/waitUntil.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/gitbook/src/lib/waitUntil.ts b/packages/gitbook/src/lib/waitUntil.ts index ed606e959d..dbaae41a0d 100644 --- a/packages/gitbook/src/lib/waitUntil.ts +++ b/packages/gitbook/src/lib/waitUntil.ts @@ -66,16 +66,9 @@ export async function waitUntil(promise: Promise<unknown>) { } } - if (GITBOOK_RUNTIME === 'vercel' && isV2()) { - // @ts-expect-error - `after` is not exported by `next/server` in next 14 - const { after } = await import('next/server'); - if (typeof after === 'function') { - after(() => promise); - return; - } - } - - await promise; + await promise.catch((error) => { + console.error('Ignored error in waitUntil', error); + }); } /** From 28a9ee701e17a256387a301a078b5310c283d530 Mon Sep 17 00:00:00 2001 From: spastorelli <spastorelli@users.noreply.github.com> Date: Fri, 13 Jun 2025 12:10:10 +0200 Subject: [PATCH 102/127] Add paypal.gitbook.com to ADAPTIVE_CONTENT_HOSTS list (#3315) --- packages/gitbook-v2/src/middleware.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index c11d466c78..0d57ce27e2 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -39,6 +39,7 @@ type URLWithMode = { url: URL; mode: 'url' | 'url-host' }; */ const ADAPTIVE_CONTENT_HOSTS = [ 'docs.gitbook.com', + 'paypal.gitbook.com', 'adaptive-docs.gitbook-staging.com', 'enriched-content-playground.gitbook-staging.io', 'docs.testgitbook.com', From a28a9978517ae19c08b8dd3b61c98af2f7791d24 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Fri, 13 Jun 2025 15:42:00 +0200 Subject: [PATCH 103/127] Add margin to adjacent buttons (#3316) --- .changeset/purple-cougars-breathe.md | 5 ++++ .../components/DocumentView/InlineButton.tsx | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 .changeset/purple-cougars-breathe.md diff --git a/.changeset/purple-cougars-breathe.md b/.changeset/purple-cougars-breathe.md new file mode 100644 index 0000000000..2c6d010d4e --- /dev/null +++ b/.changeset/purple-cougars-breathe.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add margin to adjacent buttons diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx index 9841c02626..a36cd74527 100644 --- a/packages/gitbook/src/components/DocumentView/InlineButton.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx @@ -17,18 +17,22 @@ export async function InlineButton(props: InlineProps<api.DocumentInlineButton>) } return ( - <Button - href={resolved.href} - label={inline.data.label} - // TODO: use a variant specifically for user-defined buttons. - variant={inline.data.kind} - insights={{ - type: 'link_click', - link: { - target: inline.data.ref, - position: api.SiteInsightsLinkPosition.Content, - }, - }} - /> + // Set the leading to have some vertical space between adjacent buttons + <span className="inline-button leading-[3rem] [&:has(+.inline-button)]:mr-2"> + <Button + href={resolved.href} + label={inline.data.label} + // TODO: use a variant specifically for user-defined buttons. + variant={inline.data.kind} + className="leading-normal" + insights={{ + type: 'link_click', + link: { + target: inline.data.ref, + position: api.SiteInsightsLinkPosition.Content, + }, + }} + /> + </span> ); } From 6294bbb53cffc9f7eb7487bd1a375b3c57622b17 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Fri, 13 Jun 2025 15:46:37 +0200 Subject: [PATCH 104/127] Fix error propagating (#3317) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .changeset/rotten-donuts-bow.md | 5 +++++ packages/gitbook-v2/src/app/global-error.tsx | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .changeset/rotten-donuts-bow.md create mode 100644 packages/gitbook-v2/src/app/global-error.tsx diff --git a/.changeset/rotten-donuts-bow.md b/.changeset/rotten-donuts-bow.md new file mode 100644 index 0000000000..d6ebe0650a --- /dev/null +++ b/.changeset/rotten-donuts-bow.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +add a global error boundary diff --git a/packages/gitbook-v2/src/app/global-error.tsx b/packages/gitbook-v2/src/app/global-error.tsx new file mode 100644 index 0000000000..abf0186a40 --- /dev/null +++ b/packages/gitbook-v2/src/app/global-error.tsx @@ -0,0 +1,18 @@ +'use client'; + +import NextError from 'next/error'; + +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; +}) { + console.error('Global error:', error); + return ( + <html lang="en"> + <body> + <NextError statusCode={undefined as any} /> + </body> + </html> + ); +} From 8a3910e208d83a0048e61f06e09e26cac263aef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Fri, 13 Jun 2025 15:55:04 +0200 Subject: [PATCH 105/127] Experiment with hiding sections without a matching site space (#3318) --- .../SiteSections/encodeClientSiteSections.ts | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts index 1c8cd09899..891936b6fc 100644 --- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts +++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts @@ -1,5 +1,5 @@ import { getSectionURL, getSiteSpaceURL } from '@/lib/sites'; -import type { SiteSection, SiteSectionGroup } from '@gitbook/api'; +import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api'; import type { GitBookSiteContext, SiteSections } from '@v2/lib/context'; export type ClientSiteSections = { @@ -33,10 +33,14 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections: title: item.title, icon: item.icon, object: item.object, - sections: item.sections.map((section) => encodeSection(context, section)), + sections: item.sections + .filter((section) => shouldIncludeSection(context, section)) + .map((section) => encodeSection(context, section)), }); } else { - clientSections.push(encodeSection(context, item)); + if (shouldIncludeSection(context, item)) { + clientSections.push(encodeSection(context, item)); + } } } @@ -57,6 +61,33 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) { }; } +/** + * Test if a section should be included in the list of sections. + */ +function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) { + if (context.site.id !== 'site_JOVzv') { + return true; + } + + // Testing for a new mode of navigation where the multi-variants section are hidden + // if they do not include an equivalent of the current site space. + + // TODO: replace with a proper flag on the section + const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm'; + + if (!withNavigateOnlyIfEquivalent) { + return true; + } + + const { siteSpace: currentSiteSpace } = context; + if (section.siteSpaces.length === 1) { + return true; + } + return section.siteSpaces.some((siteSpace) => + areSiteSpacesEquivalent(siteSpace, currentSiteSpace) + ); +} + /** * Find the best default site space to navigate to for a givent section: * 1. If we are on the default, continue on the default. @@ -70,8 +101,8 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) { return getSectionURL(context, section); } - const bestMatch = section.siteSpaces.find( - (siteSpace) => siteSpace.path === currentSiteSpace.path + const bestMatch = section.siteSpaces.find((siteSpace) => + areSiteSpacesEquivalent(siteSpace, currentSiteSpace) ); if (bestMatch) { return getSiteSpaceURL(context, bestMatch); @@ -79,3 +110,10 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) { return getSectionURL(context, section); } + +/** + * Test if 2 site spaces are equivalent. + */ +function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) { + return siteSpace1.path === siteSpace2.path; +} From 87218062becd2272d66c7c93f0237461c6479a2a Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Sat, 14 Jun 2025 11:10:26 +0200 Subject: [PATCH 106/127] Use Promise.all whenever possible (#3319) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../src/components/Ads/AdClassicRendering.tsx | 12 ++-- .../gitbook/src/components/Ads/renderAd.tsx | 6 +- .../DocumentView/Table/RecordCard.tsx | 16 +++--- .../src/components/PageBody/PageCover.tsx | 6 +- .../src/components/SiteLayout/SiteLayout.tsx | 55 +++++++++++-------- packages/gitbook/src/routes/ogimage.tsx | 10 ++-- 6 files changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx index e6d263bf6a..82613af491 100644 --- a/packages/gitbook/src/components/Ads/AdClassicRendering.tsx +++ b/packages/gitbook/src/components/Ads/AdClassicRendering.tsx @@ -19,14 +19,14 @@ export async function AdClassicRendering({ insightsAd: SiteInsightsAd | null; context: GitBookBaseContext; }) { - const smallImgSrc = + const [smallImgSrc, logoSrc] = await Promise.all([ 'smallImage' in ad - ? await getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 }) - : null; - const logoSrc = + ? getResizedImageURL(context.imageResizer, ad.smallImage, { width: 192, dpr: 2 }) + : null, 'logo' in ad - ? await getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 }) - : null; + ? getResizedImageURL(context.imageResizer, ad.logo, { width: 192 - 48, dpr: 2 }) + : null, + ]); return ( <Link rel="sponsored noopener" diff --git a/packages/gitbook/src/components/Ads/renderAd.tsx b/packages/gitbook/src/components/Ads/renderAd.tsx index 146f0d2530..96ff7bd3ba 100644 --- a/packages/gitbook/src/components/Ads/renderAd.tsx +++ b/packages/gitbook/src/components/Ads/renderAd.tsx @@ -43,10 +43,12 @@ interface FetchPlaceholderAdOptions { * and properly access user-agent and IP. */ export async function renderAd(options: FetchAdOptions) { - const context = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const [context, result] = await Promise.all([ + isV2() ? getServerActionBaseContext() : getV1BaseContext(), + options.source === 'live' ? fetchAd(options) : getPlaceholderAd(), + ]); const mode = options.source === 'live' ? options.mode : 'classic'; - const result = options.source === 'live' ? await fetchAd(options) : await getPlaceholderAd(); if (!result || !result.ad.description || !result.ad.statlink) { return null; } diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index 4bc1e0ce85..c32fb01e9d 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -23,18 +23,18 @@ export async function RecordCard( const coverFile = view.coverDefinition ? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0] : null; - const cover = - coverFile && context.contentContext - ? await resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext) - : null; - const targetRef = view.targetDefinition ? (record[1].values[view.targetDefinition] as ContentRef) : null; - const target = + + const [cover, target] = await Promise.all([ + coverFile && context.contentContext + ? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext) + : null, targetRef && context.contentContext - ? await resolveContentRef(targetRef, context.contentContext) - : null; + ? resolveContentRef(targetRef, context.contentContext) + : null, + ]); const coverIsSquareOrPortrait = cover?.file?.dimensions && diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index a4bb712618..3b37dfd389 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -21,8 +21,10 @@ export async function PageCover(props: { context: GitBookSiteContext; }) { const { as, page, cover, context } = props; - const resolved = cover.ref ? await resolveContentRef(cover.ref, context) : null; - const resolvedDark = cover.refDark ? await resolveContentRef(cover.refDark, context) : null; + const [resolved, resolvedDark] = await Promise.all([ + cover.ref ? resolveContentRef(cover.ref, context) : null, + cover.refDark ? resolveContentRef(cover.refDark, context) : null, + ]); return ( <div diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx index dda7095266..83211354f1 100644 --- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx +++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx @@ -102,30 +102,37 @@ export async function generateSiteLayoutMetadata(context: GitBookSiteContext): P const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null; const faviconSize = 48; - const icons = [ - { - url: customIcon?.light - ? await getResizedImageURL(imageResizer, customIcon.light, { - width: faviconSize, - height: faviconSize, - }) - : linker.toAbsoluteURL( - linker.toPathInSpace('~gitbook/icon?size=small&theme=light') - ), - type: 'image/png', - media: '(prefers-color-scheme: light)', - }, - { - url: customIcon?.dark - ? await getResizedImageURL(imageResizer, customIcon.dark, { - width: faviconSize, - height: faviconSize, - }) - : linker.toAbsoluteURL(linker.toPathInSpace('~gitbook/icon?size=small&theme=dark')), - type: 'image/png', - media: '(prefers-color-scheme: dark)', - }, - ]; + const icons = await Promise.all( + [ + { + url: customIcon?.light + ? getResizedImageURL(imageResizer, customIcon.light, { + width: faviconSize, + height: faviconSize, + }) + : linker.toAbsoluteURL( + linker.toPathInSpace('~gitbook/icon?size=small&theme=light') + ), + type: 'image/png', + media: '(prefers-color-scheme: light)', + }, + { + url: customIcon?.dark + ? getResizedImageURL(imageResizer, customIcon.dark, { + width: faviconSize, + height: faviconSize, + }) + : linker.toAbsoluteURL( + linker.toPathInSpace('~gitbook/icon?size=small&theme=dark') + ), + type: 'image/png', + media: '(prefers-color-scheme: dark)', + }, + ].map(async (icon) => ({ + ...icon, + url: await icon.url, + })) + ); return { title: site.title, diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index e55dcfc2dd..e213ee2238 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -45,7 +45,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page : ''; // Load the fonts - const { fontFamily, fonts } = await (async () => { + const fontLoader = async () => { // google fonts if (typeof customization.styling.font === 'string') { const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter; @@ -85,7 +85,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page ).filter(filterOutNullable); return { fontFamily: 'CustomFont', fonts }; - })(); + }; const theme = customization.themes.default; const useLightTheme = theme === 'light'; @@ -139,7 +139,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page break; } - const favicon = await (async () => { + const faviconLoader = async () => { if ('icon' in customization.favicon) return ( <img @@ -164,7 +164,9 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page ) ); return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />; - })(); + }; + + const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]); return new ImageResponse( <div From 2863fe0dc17e2bf20dc8cb4137f72480024cc73a Mon Sep 17 00:00:00 2001 From: Taran Vohra <taranvohra@outlook.com> Date: Mon, 16 Jun 2025 09:59:55 +0530 Subject: [PATCH 107/127] Use `resolvePublishedContentByUrl` instead of the deprecated resolution endpoint (#3310) --- packages/gitbook-v2/src/lib/data/lookup.ts | 97 +++++----------------- packages/gitbook-v2/src/middleware.ts | 20 +---- 2 files changed, 24 insertions(+), 93 deletions(-) diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts index 4c999bd7a4..be8c21f1d6 100644 --- a/packages/gitbook-v2/src/lib/data/lookup.ts +++ b/packages/gitbook-v2/src/lib/data/lookup.ts @@ -1,7 +1,7 @@ import { race, tryCatch } from '@/lib/async'; import { joinPath, joinPathWithBaseURL } from '@/lib/paths'; import { trace } from '@/lib/tracing'; -import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api'; +import type { PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api'; import { apiClient } from './api'; import { getExposableError } from './errors'; import type { DataFetcherResponse } from './types'; @@ -18,85 +18,32 @@ interface LookupPublishedContentByUrlInput { * Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint. * To optimize caching, we try multiple lookup alternatives and return the first one that matches. */ -export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) { - return lookupPublishedContentByUrl({ - url: input.url, - fetchLookupAPIResult: ({ url, signal }) => { - const api = apiClient({ apiToken: input.apiToken }); - return trace( - { - operation: 'resolvePublishedContentByUrl', - name: url, - }, - () => - tryCatch( - api.urls.resolvePublishedContentByUrl( - { - url, - ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}), - redirectOnError: input.redirectOnError, - }, - { signal } - ) - ) - ); - }, - }); -} - -/** - * Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint. - * To optimize caching, we try multiple lookup alternatives and return the first one that matches. - * - * @deprecated use resolvePublishedContentByUrl. - * - */ -export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) { - return lookupPublishedContentByUrl({ - url: input.url, - fetchLookupAPIResult: ({ url, signal }) => { - const api = apiClient({ apiToken: input.apiToken }); - return trace( - { - operation: 'getPublishedContentByURL', - name: url, - }, - () => - tryCatch( - api.urls.getPublishedContentByUrl( - { - url, - visitorAuthToken: input.visitorPayload.jwtToken ?? undefined, - redirectOnError: input.redirectOnError, - // @ts-expect-error - cacheVersion is not a real query param - cacheVersion: 'v2', - }, - { signal } - ) - ) - ); - }, - }); -} - -type TryCatch<T> = ReturnType<typeof tryCatch<T>>; - -async function lookupPublishedContentByUrl(input: { - url: string; - fetchLookupAPIResult: (args: { - url: string; - signal: AbortSignal; - }) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>; -}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> { +export async function lookupPublishedContentByUrl( + input: LookupPublishedContentByUrlInput +): Promise<DataFetcherResponse<PublishedSiteContentLookup>> { const lookupURL = new URL(input.url); const url = stripURLSearch(lookupURL); const lookup = getURLLookupAlternatives(url); const result = await race(lookup.urls, async (alternative, { signal }) => { - const callResult = await input.fetchLookupAPIResult({ - url: alternative.url, - signal, - }); + const api = apiClient({ apiToken: input.apiToken }); + const callResult = await trace( + { + operation: 'resolvePublishedContentByUrl', + name: alternative.url, + }, + () => + tryCatch( + api.urls.resolvePublishedContentByUrl( + { + url: alternative.url, + ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}), + redirectOnError: input.redirectOnError, + }, + { signal } + ) + ) + ); if (callResult.error) { if (alternative.primary) { diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 0d57ce27e2..d413d6a435 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -16,10 +16,9 @@ import { import { serveResizedImage } from '@/routes/image'; import { DataFetcherError, - getPublishedContentByURL, getVisitorAuthBasePath, + lookupPublishedContentByUrl, normalizeURL, - resolvePublishedContentByUrl, throwIfDataError, } from '@v2/lib/data'; import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env'; @@ -34,18 +33,6 @@ export const config = { type URLWithMode = { url: URL; mode: 'url' | 'url-host' }; -/** - * Temporary list of hosts to test adaptive content using the new resolution API. - */ -const ADAPTIVE_CONTENT_HOSTS = [ - 'docs.gitbook.com', - 'paypal.gitbook.com', - 'adaptive-docs.gitbook-staging.com', - 'enriched-content-playground.gitbook-staging.io', - 'docs.testgitbook.com', - 'launchdarkly-site.gitbook.education', -]; - export async function middleware(request: NextRequest) { try { const requestURL = new URL(request.url); @@ -104,11 +91,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { }); const withAPIToken = async (apiToken: string | null) => { - const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname) - ? resolvePublishedContentByUrl - : getPublishedContentByURL; const siteURLData = await throwIfDataError( - resolve({ + lookupPublishedContentByUrl({ url: siteRequestURL.toString(), visitorPayload: { jwtToken: visitorToken?.token ?? undefined, From 42d88da73ce7d0a71e20c60625571bc773443c56 Mon Sep 17 00:00:00 2001 From: Utku Ufuk <utkuufuk@gmail.com> Date: Mon, 16 Jun 2025 12:31:15 +0200 Subject: [PATCH 108/127] Fix UX issue about highlighting the search term in search result sections (#3323) --- .changeset/afraid-gifts-sparkle.md | 5 +++++ README.md | 10 ++++++++-- .../Search/SearchSectionResultItem.tsx | 17 ++++++++++++----- 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 .changeset/afraid-gifts-sparkle.md diff --git a/.changeset/afraid-gifts-sparkle.md b/.changeset/afraid-gifts-sparkle.md new file mode 100644 index 0000000000..ec33c1efcf --- /dev/null +++ b/.changeset/afraid-gifts-sparkle.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix UX issue about highlighting the search term in search result sections diff --git a/README.md b/README.md index 8a9f5c2fdc..9b13767ffd 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,19 @@ git clone https://github.com/gitbookIO/gitbook.git bun install ``` -4. Start your local development server. +4. Run build. + +``` +bun build:v2 +``` + +5. Start your local development server. ``` bun dev:v2 ``` -5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`. +6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`. examples: diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx index 960b1eb38a..a0f467f80f 100644 --- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx @@ -66,11 +66,7 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe <HighlightQuery query={query} text={item.title} /> </p> ) : null} - {item.body ? ( - <p className={tcls('text-sm', 'line-clamp-3', 'relative')}> - <HighlightQuery query={query} text={item.body} /> - </p> - ) : null} + {item.body ? highlightQueryInBody(item.body, query) : null} </div> <div className={tcls( @@ -90,3 +86,14 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe </Link> ); }); + +function highlightQueryInBody(body: string, query: string) { + const idx = body.indexOf(query); + + // Ensure the query to be highlighted is visible in the body. + return ( + <p className={tcls('text-sm', 'line-clamp-3', 'relative')}> + <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 15)}`} /> + </p> + ); +} From 72cd0e59e683ace3d269f0e59338b41c36b88651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 16 Jun 2025 13:37:45 +0200 Subject: [PATCH 109/127] Replace withoutConcurrency by a smarter React.cache (#3325) --- .changeset/fuzzy-tables-jump.md | 5 + bun.lock | 3 + packages/gitbook-v2/package.json | 3 +- packages/gitbook-v2/src/lib/cache.test.ts | 80 ++ packages/gitbook-v2/src/lib/cache.ts | 59 ++ packages/gitbook-v2/src/lib/data/api.ts | 979 +++++++++----------- packages/gitbook-v2/src/lib/data/memoize.ts | 92 -- 7 files changed, 609 insertions(+), 612 deletions(-) create mode 100644 .changeset/fuzzy-tables-jump.md create mode 100644 packages/gitbook-v2/src/lib/cache.test.ts create mode 100644 packages/gitbook-v2/src/lib/cache.ts delete mode 100644 packages/gitbook-v2/src/lib/data/memoize.ts diff --git a/.changeset/fuzzy-tables-jump.md b/.changeset/fuzzy-tables-jump.md new file mode 100644 index 0000000000..ada33d1c55 --- /dev/null +++ b/.changeset/fuzzy-tables-jump.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Optimize performances by using a smarter per-request cache arround data cached functions diff --git a/bun.lock b/bun.lock index 3ff6d61c4c..b4d2b49d1f 100644 --- a/bun.lock +++ b/bun.lock @@ -169,6 +169,7 @@ "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", "next": "^15.3.2", + "object-identity": "^0.1.2", "react": "^19.0.0", "react-dom": "^19.0.0", "rison": "^0.1.1", @@ -2438,6 +2439,8 @@ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="], + "object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="], "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 5723cdbc79..cfa6e71b91 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -14,7 +14,8 @@ "react-dom": "^19.0.0", "rison": "^0.1.1", "server-only": "^0.0.1", - "warn-once": "^0.1.1" + "warn-once": "^0.1.1", + "object-identity": "^0.1.2" }, "devDependencies": { "gitbook": "*", diff --git a/packages/gitbook-v2/src/lib/cache.test.ts b/packages/gitbook-v2/src/lib/cache.test.ts new file mode 100644 index 0000000000..735b1fb06d --- /dev/null +++ b/packages/gitbook-v2/src/lib/cache.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'bun:test'; +import { withStableRef } from './cache'; + +describe('withStableRef', () => { + it('should return primitive values as is', () => { + const toStableRef = withStableRef(); + + expect(toStableRef(42)).toBe(42); + expect(toStableRef('hello')).toBe('hello'); + expect(toStableRef(true)).toBe(true); + expect(toStableRef(null)).toBe(null); + expect(toStableRef(undefined)).toBe(undefined); + }); + + it('should return the same reference for identical objects', () => { + const toStableRef = withStableRef(); + + const obj1 = { a: 1, b: 2 }; + const obj2 = { a: 1, b: 2 }; + + const ref1 = toStableRef(obj1); + const ref2 = toStableRef(obj2); + + expect(ref1).toBe(ref2); + expect(ref1).toBe(obj1); + expect(ref1).not.toBe(obj2); + }); + + it('should return the same reference for identical arrays', () => { + const toStableRef = withStableRef(); + + const arr1 = [1, 2, 3]; + const arr2 = [1, 2, 3]; + + const ref1 = toStableRef(arr1); + const ref2 = toStableRef(arr2); + + expect(ref1).toBe(ref2); + expect(ref1).toBe(arr1); + expect(ref1).not.toBe(arr2); + }); + + it('should return the same reference for identical nested objects', () => { + const toStableRef = withStableRef(); + + const obj1 = { a: { b: 1 }, c: [2, 3] }; + const obj2 = { a: { b: 1 }, c: [2, 3] }; + + const ref1 = toStableRef(obj1); + const ref2 = toStableRef(obj2); + + expect(ref1).toBe(ref2); + expect(ref1).toBe(obj1); + expect(ref1).not.toBe(obj2); + }); + + it('should return different references for different objects', () => { + const toStableRef = withStableRef(); + + const obj1 = { a: 1 }; + const obj2 = { a: 2 }; + + const ref1 = toStableRef(obj1); + const ref2 = toStableRef(obj2); + + expect(ref1).not.toBe(ref2); + }); + + it('should maintain reference stability across multiple calls', () => { + const toStableRef = withStableRef(); + + const obj = { a: 1 }; + const ref1 = toStableRef(obj); + const ref2 = toStableRef(obj); + const ref3 = toStableRef(obj); + + expect(ref1).toBe(ref2); + expect(ref2).toBe(ref3); + }); +}); diff --git a/packages/gitbook-v2/src/lib/cache.ts b/packages/gitbook-v2/src/lib/cache.ts new file mode 100644 index 0000000000..e05753907a --- /dev/null +++ b/packages/gitbook-v2/src/lib/cache.ts @@ -0,0 +1,59 @@ +import { identify } from 'object-identity'; +import * as React from 'react'; + +/** + * Equivalent to `React.cache` but with support for non-primitive arguments. + * As `React.cache` only uses `Object.is` to compare arguments, it will not work with non-primitive arguments. + */ +export function cache<Args extends any[], Return>(fn: (...args: Args) => Return) { + const cached = React.cache(fn); + + return (...args: Args) => { + const toStableRef = getWithStableRef(); + const stableArgs = args.map((value) => { + return toStableRef(value); + }) as Args; + return cached(...stableArgs); + }; +} + +/** + * To ensure memory is garbage collected between each request, we use a per-request cache to store the ref maps. + */ +const getWithStableRef = React.cache(withStableRef); + +/** + * Create a function that converts a value to a stable reference. + */ +export function withStableRef(): <T>(value: T) => T { + const reverseIndex = new WeakMap<object, string>(); + const refIndex = new Map<string, object>(); + + return <T>(value: T) => { + if (isPrimitive(value)) { + return value; + } + + const objectValue = value as object; + const index = reverseIndex.get(objectValue); + if (index !== undefined) { + return refIndex.get(index) as T; + } + + const hash = identify(objectValue); + reverseIndex.set(objectValue, hash); + + const existing = refIndex.get(hash); + if (existing !== undefined) { + return existing as T; + } + + // first time we've seen this shape + refIndex.set(hash, objectValue); + return value; + }; +} + +function isPrimitive(value: any): boolean { + return value === null || typeof value !== 'object'; +} diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 88c95c4016..d36c2445f5 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -8,8 +8,8 @@ import { import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; +import { cache } from '../cache'; import { DataFetcherError, wrapDataFetcherError } from './errors'; -import { withCacheKey, withoutConcurrentExecution } from './memoize'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -204,307 +204,316 @@ export function createDataFetcher( }; } -const getUserById = withCacheKey( - withoutConcurrentExecution(async (_, input: DataFetcherInput, params: { userId: string }) => { +const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, + }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + }); +}); + +const getSpace = cache( + async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => { 'use cache'; - return trace(`getUserById(${params.userId})`, async () => { + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); + + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.users.getUserById(params.userId, { - ...noCacheFetchOptions, - }); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, + } + ); cacheTag(...getCacheTagsFromResponse(res)); cacheLife('days'); return res.data; }); }); - }) + } ); -const getSpace = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; shareKey: string | undefined } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); +const getChangeRequest = cache( + async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); + + return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); + }); + } +); - return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { +const getRevision = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); + } +); + +const getRevisionPages = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); + } +); + +const getRevisionFile = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; fileId: string } + ) => { + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.spaces.getSpaceById( + const res = await api.spaces.getFileInRevisionById( params.spaceId, - { - shareKey: params.shareKey, - }, + params.revisionId, + params.fileId, + {}, { ...noCacheFetchOptions, } ); cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); + cacheLife('max'); return res.data; }); - }); - } - ) -); - -const getChangeRequest = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; changeRequestId: string } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'change-request', - space: params.spaceId, - changeRequest: params.changeRequestId, - }) - ); - - return trace( - `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - return res.data; - }); - } - ); - } - ) + } + ); + } ); -const getRevision = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } - ) => { - 'use cache'; - return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { +const getRevisionPageMarkdown = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.spaces.getRevisionById( + const res = await api.spaces.getPageInRevisionById( params.spaceId, params.revisionId, + params.pageId, { - metadata: params.metadata, + format: 'markdown', }, { ...noCacheFetchOptions, } ); + cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); - return res.data; + + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); + } + return res.data.markdown; }); - }); - } - ) + } + ); + } ); -const getRevisionPages = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; metadata: boolean } - ) => { - 'use cache'; - return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { +const getRevisionPageDocument = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.spaces.listPagesInRevisionById( + const res = await api.spaces.getPageDocumentInRevisionById( params.spaceId, params.revisionId, + params.pageId, { - metadata: params.metadata, + evaluated: true, }, { ...noCacheFetchOptions, } ); + cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); - return res.data.pages; - }); - }); - } - ) -); -const getRevisionFile = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; fileId: string } - ) => { - 'use cache'; - return trace( - `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); - } - ) + return res.data; + }); + } + ); + } ); -const getRevisionPageMarkdown = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } - ) => { - 'use cache'; - return trace( - `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - format: 'markdown', - }, - { - ...noCacheFetchOptions, - } - ); - - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); +const getRevisionPageByPath = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; path: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, } - return res.data.markdown; - }); - } - ); - } - ) + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } + ); + } ); -const getRevisionPageDocument = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; pageId: string } - ) => { - 'use cache'; - return trace( - `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageDocumentInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - evaluated: true, - }, - { - ...noCacheFetchOptions, - } - ); - - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - - return res.data; - }); - } - ); - } - ) +const getDocument = cache( + async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); + } ); -const getRevisionPageByPath = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; path: string } - ) => { - 'use cache'; - return trace( - `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); +const getComputedDocument = cache( + async ( + input: DataFetcherInput, + params: { + spaceId: string; + organizationId: string; + source: ComputedContentSource; + seed: string; } - ) -); - -const getDocument = withCacheKey( - withoutConcurrentExecution( - async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { - 'use cache'; - return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + ) => { + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( + { + spaceId: params.spaceId, + organizationId: params.organizationId, + }, + params.source + ) + ); + + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.spaces.getDocumentById( + const res = await api.spaces.getComputedDocument( params.spaceId, - params.documentId, + { + source: params.source, + seed: params.seed, + }, {}, { ...noCacheFetchOptions, @@ -514,319 +523,251 @@ const getDocument = withCacheKey( cacheLife('max'); return res.data; }); - }); - } - ) -); - -const getComputedDocument = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; } - ) => { - 'use cache'; - cacheTag( - ...getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ) - ); - - return trace( - `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getComputedDocument( - params.spaceId, - { - source: params.source, - seed: params.seed, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); - } - ) -); - -const getReusableContent = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { spaceId: string; revisionId: string; reusableContentId: string } - ) => { - 'use cache'; - return trace( - `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); - } - ) -); - -const getLatestOpenAPISpecVersionContent = withCacheKey( - withoutConcurrentExecution( - async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'openapi', - organization: params.organizationId, - openAPISpec: params.slug, - }) - ); - - return trace( - `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('max'); - return res.data; - }); - } - ); - } - ) + ); + } ); -const getPublishedContentSite = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { organizationId: string; siteId: string; siteShareKey: string | undefined } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - - return trace( - `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - } - ); - } - ) +const getReusableContent = cache( + async ( + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; reusableContentId: string } + ) => { + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } + ); + } ); -const getSiteRedirectBySource = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; +const getLatestOpenAPISpecVersionContent = cache( + async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'openapi', + organization: params.organizationId, + openAPISpec: params.slug, + }) + ); + + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - - return trace( - `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - }, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; - }); - } - ); - } - ) + ); + } ); -const getEmbedByUrl = withCacheKey( - withoutConcurrentExecution( - async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - - return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { +const getPublishedContentSite = cache( + async ( + input: DataFetcherInput, + params: { organizationId: string; siteId: string; siteShareKey: string | undefined } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); + + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { return wrapDataFetcherError(async () => { const api = apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace( - params.spaceId, + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, { - url: params.url, + shareKey: params.siteShareKey, }, { ...noCacheFetchOptions, } ); cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('weeks'); + cacheLife('days'); return res.data; }); - }); - } - ) + } + ); + } ); -const searchSiteContent = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - - return trace( - `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, scope } = params; - const api = apiClient(input); - const res = await api.orgs.searchSiteContent( - organizationId, - siteId, - { - query, - ...scope, - }, - {}, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('hours'); - return res.data.items; - }); - } - ); +const getSiteRedirectBySource = cache( + async ( + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; } - ) + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); + + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + } + ); + } ); -const renderIntegrationUi = withCacheKey( - withoutConcurrentExecution( - async ( - _, - input: DataFetcherInput, - params: { integrationName: string; request: RenderIntegrationUI } - ) => { - 'use cache'; - cacheTag( - getCacheTag({ - tag: 'integration', - integration: params.integrationName, - }) - ); +const getEmbedByUrl = cache( + async (input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); + + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); + }); + } +); - return trace(`renderIntegrationUi(${params.integrationName})`, async () => { +const searchSiteContent = cache( + async ( + input: DataFetcherInput, + params: Parameters<GitBookDataFetcher['searchSiteContent']>[0] + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); + + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; const api = apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request, + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, { ...noCacheFetchOptions, } ); cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('days'); - return res.data; + cacheLife('hours'); + return res.data.items; }); + } + ); + } +); + +const renderIntegrationUi = cache( + async ( + input: DataFetcherInput, + params: { integrationName: string; request: RenderIntegrationUI } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) + ); + + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; }); - } - ) + }); + } ); async function* streamAIResponse( diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts deleted file mode 100644 index 5110d015fa..0000000000 --- a/packages/gitbook-v2/src/lib/data/memoize.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { cache } from 'react'; - -// This is used to create a context specific to the current request. -// This version works both in cloudflare and in vercel. -const getRequestContext = cache(() => ({})); - -/** - * Wrap a function by preventing concurrent executions of the same function. - * With a logic to work per-request in Cloudflare Workers. - */ -export function withoutConcurrentExecution<ArgsType extends any[], ReturnType>( - wrapped: (key: string, ...args: ArgsType) => Promise<ReturnType> -): (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> { - const globalPromiseCache = new WeakMap<object, Map<string, Promise<ReturnType>>>(); - - return (key: string, ...args: ArgsType) => { - const globalContext = getRequestContext(); - - /** - * Cache storage that is scoped to the current request when executed in Cloudflare Workers, - * to avoid "Cannot perform I/O on behalf of a different request" errors. - */ - const promiseCache = - globalPromiseCache.get(globalContext) ?? new Map<string, Promise<ReturnType>>(); - globalPromiseCache.set(globalContext, promiseCache); - - const concurrent = promiseCache.get(key); - if (concurrent) { - return concurrent; - } - - const promise = (async () => { - try { - const result = await wrapped(key, ...args); - return result; - } finally { - promiseCache.delete(key); - } - })(); - - promiseCache.set(key, promise); - - return promise; - }; -} - -/** - * Wrap a function by passing it a cache key that is computed from the function arguments. - */ -export function withCacheKey<ArgsType extends any[], ReturnType>( - wrapped: (cacheKey: string, ...args: ArgsType) => Promise<ReturnType> -): (...args: ArgsType) => Promise<ReturnType> { - return (...args: ArgsType) => { - const cacheKey = getCacheKey(args); - return wrapped(cacheKey, ...args); - }; -} - -/** - * Compute a cache key from the function arguments. - */ -function getCacheKey(args: any[]) { - return JSON.stringify(deepSortValue(args)); -} - -function deepSortValue(value: unknown): unknown { - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' || - value === null || - value === undefined - ) { - return value; - } - - if (Array.isArray(value)) { - return value.map(deepSortValue); - } - - if (value && typeof value === 'object') { - return Object.entries(value) - .map(([key, subValue]) => { - return [key, deepSortValue(subValue)] as const; - }) - .sort((a, b) => { - return a[0].localeCompare(b[0]); - }); - } - - return value; -} From d99da6a3ae7f128116bbec9ade8dcbe01f94eb31 Mon Sep 17 00:00:00 2001 From: Utku Ufuk <utkuufuk@gmail.com> Date: Mon, 16 Jun 2025 14:33:00 +0200 Subject: [PATCH 110/127] Ignore case while pattern-matching to highlight search results (#3326) --- .changeset/slimy-cows-press.md | 5 +++++ .../src/components/Search/SearchSectionResultItem.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/slimy-cows-press.md diff --git a/.changeset/slimy-cows-press.md b/.changeset/slimy-cows-press.md new file mode 100644 index 0000000000..17ec44ca8a --- /dev/null +++ b/.changeset/slimy-cows-press.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Ignore case while highlighting search results. diff --git a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx index a0f467f80f..8ccda684d2 100644 --- a/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx +++ b/packages/gitbook/src/components/Search/SearchSectionResultItem.tsx @@ -88,12 +88,12 @@ export const SearchSectionResultItem = React.forwardRef(function SearchSectionRe }); function highlightQueryInBody(body: string, query: string) { - const idx = body.indexOf(query); + const idx = body.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()); // Ensure the query to be highlighted is visible in the body. return ( <p className={tcls('text-sm', 'line-clamp-3', 'relative')}> - <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 15)}`} /> + <HighlightQuery query={query} text={idx < 20 ? body : `...${body.slice(idx - 10)}`} /> </p> ); } From 382a19885be802945ee309e71d4699ccd720c94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= <berge.greg@gmail.com> Date: Mon, 16 Jun 2025 15:52:42 +0200 Subject: [PATCH 111/127] Hide empty section groups (#3327) --- .../SiteSections/encodeClientSiteSections.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts index 891936b6fc..98c22bbb66 100644 --- a/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts +++ b/packages/gitbook/src/components/SiteSections/encodeClientSiteSections.ts @@ -1,6 +1,7 @@ import { getSectionURL, getSiteSpaceURL } from '@/lib/sites'; import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api'; import type { GitBookSiteContext, SiteSections } from '@v2/lib/context'; +import assertNever from 'assert-never'; export type ClientSiteSections = { list: (ClientSiteSection | ClientSiteSectionGroup)[]; @@ -27,20 +28,32 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections: const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = []; for (const item of list) { - if (item.object === 'site-section-group') { - clientSections.push({ - id: item.id, - title: item.title, - icon: item.icon, - object: item.object, - sections: item.sections + switch (item.object) { + case 'site-section-group': { + const sections = item.sections .filter((section) => shouldIncludeSection(context, section)) - .map((section) => encodeSection(context, section)), - }); - } else { - if (shouldIncludeSection(context, item)) { + .map((section) => encodeSection(context, section)); + + // Skip empty groups + if (sections.length === 0) { + continue; + } + + clientSections.push({ + id: item.id, + title: item.title, + icon: item.icon, + object: item.object, + sections, + }); + continue; + } + case 'site-section': { clientSections.push(encodeSection(context, item)); + continue; } + default: + assertNever(item, 'Unknown site section object type'); } } From b7a0db3339dcdfb882a5e46e90a989bb1eb088a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 16 Jun 2025 16:59:54 +0200 Subject: [PATCH 112/127] Fix rendering of ogimage with SVG logos (#3328) --- .changeset/green-clouds-cough.md | 5 ++ bun.lock | 3 + packages/gitbook/package.json | 3 +- packages/gitbook/src/routes/ogimage.tsx | 85 +++++++++++-------------- 4 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 .changeset/green-clouds-cough.md diff --git a/.changeset/green-clouds-cough.md b/.changeset/green-clouds-cough.md new file mode 100644 index 0000000000..0988603814 --- /dev/null +++ b/.changeset/green-clouds-cough.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Fix rendering of ogimage with SVG logos. diff --git a/bun.lock b/bun.lock index b4d2b49d1f..1c192ca717 100644 --- a/bun.lock +++ b/bun.lock @@ -87,6 +87,7 @@ "classnames": "^2.5.1", "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", + "image-size": "^2.0.2", "js-cookie": "^3.0.5", "jsontoxml": "^1.0.1", "jwt-decode": "^4.0.0", @@ -2067,6 +2068,8 @@ "ignore": ["ignore@7.0.3", "", {}, "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA=="], + "image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-lazy": ["import-lazy@2.1.0", "", {}, "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A=="], diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 379f975a1e..2be76259d9 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -81,7 +81,8 @@ "usehooks-ts": "^3.1.0", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "image-size": "^2.0.2" }, "devDependencies": { "@argos-ci/playwright": "^5.0.3", diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index e213ee2238..653fc8fc3b 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -1,6 +1,7 @@ import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api'; import { colorContrast } from '@gitbook/colors'; import { type FontWeight, getDefaultFont } from '@gitbook/fonts'; +import { imageSize } from 'image-size'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; @@ -156,14 +157,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))} </span> ); - const src = await readSelfImage( + const iconImage = await fetchImage( linker.toAbsoluteURL( linker.toPathInSpace( `~gitbook/icon?size=medium&theme=${customization.themes.default}` ) ) ); - return <img src={src} alt="Icon" width={40} height={40} tw="mr-4" />; + return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />; }; const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]); @@ -187,21 +188,23 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page {/* Grid */} <img tw="absolute inset-0 w-[100vw] h-[100vh]" - src={await readStaticImage(gridAsset)} + src={(await fetchStaticImage(gridAsset)).src} alt="Grid" /> {/* Logo */} {customization.header.logo ? ( - <img - alt="Logo" - height={60} - src={ - useLightTheme - ? customization.header.logo.light - : customization.header.logo.dark - } - /> + <div tw="flex flex-row"> + <img + {...(await fetchImage( + useLightTheme + ? customization.header.logo.light + : customization.header.logo.dark + ))} + alt="Logo" + tw="h-[60px]" + /> + </div> ) : ( <div tw="flex"> {favicon} @@ -289,34 +292,6 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { }; } -/** - * Temporary function to log some data on Cloudflare. - * TODO: remove this when we found the issue - */ -function logOnCloudflareOnly(message: string) { - if (process.env.DEBUG_CLOUDFLARE === 'true') { - // biome-ignore lint/suspicious/noConsole: <explanation> - console.log(message); - } -} - -/** - * Read an image from a response as a base64 encoded string. - */ -async function readImage(response: Response) { - const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.startsWith('image/')) { - logOnCloudflareOnly(`Invalid content type: ${contentType}, - status: ${response.status} - rayId: ${response.headers.get('cf-ray')}`); - throw new Error(`Invalid content type: ${contentType}`); - } - - const arrayBuffer = await response.arrayBuffer(); - const base64 = Buffer.from(arrayBuffer).toString('base64'); - return `data:${contentType};base64,${base64}`; -} - // biome-ignore lint/suspicious/noExplicitAny: <explanation> const staticCache = new Map<string, any>(); @@ -335,16 +310,32 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) { /** * Read a static image and cache it in memory. */ -async function readStaticImage(url: string) { - logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticCache.size}`); - return getWithCache(`static-image:${url}`, () => readSelfImage(url)); +async function fetchStaticImage(url: string) { + return getWithCache(`static-image:${url}`, () => fetchImage(url)); } /** - * Read an image from GitBook itself. + * Fetch an image from a URL and return a base64 encoded string. + * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL. */ -async function readSelfImage(url: string) { +async function fetchImage(url: string) { const response = await fetch(url); - const image = await readImage(response); - return image; + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.startsWith('image/')) { + throw new Error(`Invalid content type: ${contentType}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const base64 = buffer.toString('base64'); + const src = `data:${contentType};base64,${base64}`; + + try { + const { width, height } = imageSize(buffer); + return { src, width, height }; + } catch (error) { + console.error(`Error reading image size: ${error}`); + return { src }; + } } From af98402655945a47473828cce1b8b33e4b858683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 16 Jun 2025 19:15:22 +0200 Subject: [PATCH 113/127] Add support for inline icons (#3329) --- .changeset/curly-rules-learn.md | 5 +++ bun.lock | 4 +-- package.json | 2 +- packages/gitbook/e2e/internal.spec.ts | 5 +++ .../DocumentView/CodeBlock/highlight.ts | 33 +++++++++++++------ .../src/components/DocumentView/Inline.tsx | 3 ++ .../components/DocumentView/InlineIcon.tsx | 10 ++++++ 7 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 .changeset/curly-rules-learn.md create mode 100644 packages/gitbook/src/components/DocumentView/InlineIcon.tsx diff --git a/.changeset/curly-rules-learn.md b/.changeset/curly-rules-learn.md new file mode 100644 index 0000000000..47400e9194 --- /dev/null +++ b/.changeset/curly-rules-learn.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Add support for inline icons. diff --git a/bun.lock b/bun.lock index 1c192ca717..94b4fc3a54 100644 --- a/bun.lock +++ b/bun.lock @@ -285,7 +285,7 @@ "react-dom": "^19.0.0", }, "catalog": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "^0.121.0", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -650,7 +650,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="], + "@gitbook/api": ["@gitbook/api@0.121.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-o4/N24RM0Rg8S/3yPDjPmt6TbQF+1iZmg9q9QKxOxMqpQ2bZmMUqS7dSkeqEbEBMALx/m/x0xQlJbEJGbOwteg=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index 1faa750ba8..1cf5326c1d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "workspaces": { "packages": ["packages/*"], "catalog": { - "@gitbook/api": "^0.120.0" + "@gitbook/api": "^0.121.0" } }, "patchedDependencies": { diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 3efe00bd27..3a884ae5ea 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -603,6 +603,11 @@ const testCases: TestsCase[] = [ url: 'blocks/emojis', run: waitForCookiesDialog, }, + { + name: 'Icons', + url: 'blocks/icons', + run: waitForCookiesDialog, + }, { name: 'Links', url: 'blocks/links', diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index eb02b5b928..89d00dafba 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -12,6 +12,7 @@ import { import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import { type BundledLanguage, bundledLanguages } from 'shiki/langs'; +import { nullIfNever } from '@/lib/typescript'; import { plainHighlight } from './plain-highlight'; export type HighlightLine = { @@ -263,16 +264,28 @@ function getPlainCodeBlockLine( if (node.object === 'text') { content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); } else { - const start = index + content.length; - content += getPlainCodeBlockLine(node, index + content.length, inlines); - const end = index + content.length; - - if (inlines) { - inlines.push({ - inline: node, - start, - end, - }); + switch (node.type) { + case 'annotation': { + const start = index + content.length; + content += getPlainCodeBlockLine(node, index + content.length, inlines); + const end = index + content.length; + + if (inlines) { + inlines.push({ + inline: node, + start, + end, + }); + } + break; + } + case 'expression': { + break; + } + default: { + nullIfNever(node); + break; + } } } } diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx index 1e1b091cef..84b56decfc 100644 --- a/packages/gitbook/src/components/DocumentView/Inline.tsx +++ b/packages/gitbook/src/components/DocumentView/Inline.tsx @@ -5,6 +5,7 @@ import { Annotation } from './Annotation/Annotation'; import type { DocumentContextProps } from './DocumentView'; import { Emoji } from './Emoji'; import { InlineButton } from './InlineButton'; +import { InlineIcon } from './InlineIcon'; import { InlineImage } from './InlineImage'; import { InlineLink } from './InlineLink'; import { InlineMath } from './Math'; @@ -47,6 +48,8 @@ export function Inline<T extends DocumentInline>(props: InlineProps<T>) { return <InlineImage {...contextProps} inline={inline} />; case 'button': return <InlineButton {...contextProps} inline={inline} />; + case 'icon': + return <InlineIcon {...contextProps} inline={inline} />; case 'expression': // The GitBook API should take care of evaluating expressions. // We should never need to render them. diff --git a/packages/gitbook/src/components/DocumentView/InlineIcon.tsx b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx new file mode 100644 index 0000000000..0eca373f69 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx @@ -0,0 +1,10 @@ +import type { DocumentInlineIcon } from '@gitbook/api'; + +import { Icon, type IconName } from '@gitbook/icons'; +import type { InlineProps } from './Inline'; + +export async function InlineIcon(props: InlineProps<DocumentInlineIcon>) { + const { inline } = props; + + return <Icon icon={inline.data.icon as IconName} className="inline size-[1em]" />; +} From 11a6511b7aa7d1052c887417d97d4eede6104daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Mon, 16 Jun 2025 20:06:33 +0200 Subject: [PATCH 114/127] Fix crash when integration script fails to render block (#3332) --- .changeset/famous-melons-compete.md | 5 +++ .changeset/orange-ears-drop.md | 5 +++ packages/gitbook-v2/src/lib/data/errors.ts | 34 ++++++++++++--- .../Integration/IntegrationBlock.tsx | 34 +++++++++------ .../DocumentView/Integration/render.ts | 31 ++++++++++++++ .../Integration/server-actions.tsx | 21 ++++++---- packages/react-contentkit/src/ContentKit.tsx | 41 ++++++++++++------- 7 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 .changeset/famous-melons-compete.md create mode 100644 .changeset/orange-ears-drop.md create mode 100644 packages/gitbook/src/components/DocumentView/Integration/render.ts diff --git a/.changeset/famous-melons-compete.md b/.changeset/famous-melons-compete.md new file mode 100644 index 0000000000..17512b6f49 --- /dev/null +++ b/.changeset/famous-melons-compete.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix crash when integration script fails to render block. diff --git a/.changeset/orange-ears-drop.md b/.changeset/orange-ears-drop.md new file mode 100644 index 0000000000..12dd7fae5d --- /dev/null +++ b/.changeset/orange-ears-drop.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-contentkit": patch +--- + +Add basic error handling when transitioning between states. diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts index 784eebdbb5..4059fb5c94 100644 --- a/packages/gitbook-v2/src/lib/data/errors.ts +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -47,11 +47,7 @@ export function getDataOrNull<T>( return response.then((result) => getDataOrNull(result, ignoreErrors)); } - if (response.error) { - if (ignoreErrors.includes(response.error.code)) return null; - throw new DataFetcherError(response.error.message, response.error.code); - } - return response.data; + return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null; } /** @@ -93,6 +89,34 @@ export async function wrapDataFetcherError<T>( } } +/** + * Ignore some data fetcher errors. + */ +export function ignoreDataFetcherErrors<T>( + response: DataFetcherResponse<T>, + ignoreErrors?: number[] +): DataFetcherResponse<T>; +export function ignoreDataFetcherErrors<T>( + response: Promise<DataFetcherResponse<T>>, + ignoreErrors?: number[] +): Promise<DataFetcherResponse<T>>; +export function ignoreDataFetcherErrors<T>( + response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>, + ignoreErrors: number[] = [404] +): DataFetcherResponse<T> | Promise<DataFetcherResponse<T>> { + if (response instanceof Promise) { + return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors)); + } + + if (response.error) { + if (ignoreErrors.includes(response.error.code)) { + return response; + } + throw new DataFetcherError(response.error.message, response.error.code); + } + return response; +} + /** * Get a data fetcher exposable error from a JS error. */ diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx index 6ebec6f030..a26aa3bfd2 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx @@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env'; import type { BlockProps } from '../Block'; import './contentkit.css'; -import { getDataOrNull } from '@v2/lib/data'; import { contentKitServerContext } from './contentkit'; +import { fetchSafeIntegrationUI } from './render'; import { renderIntegrationUi } from './server-actions'; export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegration>) { @@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio throw new Error('integration block requires a content.spaceId'); } - const { dataFetcher } = context.contentContext; - const initialInput: RenderIntegrationUI = { componentId: block.data.block, props: block.data.props, @@ -30,17 +28,27 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio }, }; - const initialOutput = await getDataOrNull( - dataFetcher.renderIntegrationUi({ - integrationName: block.data.integration, - request: initialInput, - }), + const initialResponse = await fetchSafeIntegrationUI(context.contentContext, { + integrationName: block.data.integration, + request: initialInput, + }); - // The API can respond with a 400 error if the integration is not installed - // and 404 if the integration is not found. - [404, 400] - ); - if (!initialOutput || initialOutput.type === 'complete') { + if (initialResponse.error) { + if (initialResponse.error.code === 404) { + return null; + } + + return ( + <div className={tcls(style)}> + <pre> + Unexpected error with integration {block.data.integration}:{' '} + {initialResponse.error.message} + </pre> + </div> + ); + } + const initialOutput = initialResponse.data; + if (initialOutput.type === 'complete') { return null; } diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts new file mode 100644 index 0000000000..a9a86c9e59 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts @@ -0,0 +1,31 @@ +import type { RenderIntegrationUI } from '@gitbook/api'; +import type { GitBookBaseContext } from '@v2/lib/context'; +import { ignoreDataFetcherErrors } from '@v2/lib/data'; + +/** + * Render an integration UI while ignoring some errors. + */ +export async function fetchSafeIntegrationUI( + context: GitBookBaseContext, + { + integrationName, + request, + }: { + integrationName: string; + request: RenderIntegrationUI; + } +) { + const output = await ignoreDataFetcherErrors( + context.dataFetcher.renderIntegrationUi({ + integrationName, + request, + }), + + // The API can respond with a 400 error if the integration is not installed + // and 404 if the integration is not found. + // The API can also respond with a 502 error if the integration is not generating a proper response. + [404, 400, 502] + ); + + return output; +} diff --git a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx index 54180e5c89..91d1e0e73c 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx @@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1'; import { isV2 } from '@/lib/v2'; import type { RenderIntegrationUI } from '@gitbook/api'; import { ContentKitOutput } from '@gitbook/react-contentkit'; -import { throwIfDataError } from '@v2/lib/data'; import { getServerActionBaseContext } from '@v2/lib/server-actions'; import { contentKitServerContext } from './contentkit'; +import { fetchSafeIntegrationUI } from './render'; /** * Server action to render an integration UI request from <ContentKit />. @@ -22,16 +22,19 @@ export async function renderIntegrationUi({ request: RenderIntegrationUI; }) { const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const output = await fetchSafeIntegrationUI(serverAction, { + integrationName: renderContext.integrationName, + request, + }); - const output = await throwIfDataError( - serverAction.dataFetcher.renderIntegrationUi({ - integrationName: renderContext.integrationName, - request, - }) - ); + if (output.error) { + return { + error: output.error.message, + }; + } return { - children: <ContentKitOutput output={output} context={contentKitServerContext} />, - output: output, + children: <ContentKitOutput output={output.data} context={contentKitServerContext} />, + output: output.data, }; } diff --git a/packages/react-contentkit/src/ContentKit.tsx b/packages/react-contentkit/src/ContentKit.tsx index 9d53c70de2..4f413f367a 100644 --- a/packages/react-contentkit/src/ContentKit.tsx +++ b/packages/react-contentkit/src/ContentKit.tsx @@ -38,10 +38,18 @@ export function ContentKit<RenderContext>(props: { render: (input: { renderContext: RenderContext; request: RequestRenderIntegrationUI; - }) => Promise<{ - children: React.ReactNode; - output: ContentKitRenderOutput; - }>; + }) => Promise< + | { + error?: undefined; + children: React.ReactNode; + output: ContentKitRenderOutput; + } + | { + error: string; + children?: undefined; + output?: undefined; + } + >; /** Callback when an action is triggered */ onAction?: (action: ContentKitAction) => void; /** Callback when the flow is completed */ @@ -98,17 +106,18 @@ export function ContentKit<RenderContext>(props: { request: newInput, }); const output = result.output; + if (output) { + if (output.type === 'complete') { + return onComplete?.(output.returnValue); + } - if (output.type === 'complete') { - return onComplete?.(output.returnValue); + setCurrent((prev) => ({ + input: newInput, + children: result.children, + output: output, + state: prev.state, + })); } - - setCurrent((prev) => ({ - input: newInput, - children: result.children, - output: output, - state: prev.state, - })); }, [setCurrent, current, render, onComplete] ); @@ -147,8 +156,10 @@ export function ContentKit<RenderContext>(props: { renderContext, request: modalInput, }); - - if (result.output.type === 'element' || !result.output.type) { + if ( + result.output && + (result.output.type === 'element' || !result.output.type) + ) { setSubView({ mode: 'modal', initialInput: modalInput, From dfa8a37be19c1868bb1c833631f199b4f52b3305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 00:16:52 +0200 Subject: [PATCH 115/127] Don't cache unexpected API errors for more than a few minutes (#3334) --- .changeset/real-walls-glow.md | 5 + packages/gitbook-v2/src/lib/data/api.ts | 203 +++++++++++---------- packages/gitbook-v2/src/lib/data/errors.ts | 17 ++ 3 files changed, 125 insertions(+), 100 deletions(-) create mode 100644 .changeset/real-walls-glow.md diff --git a/.changeset/real-walls-glow.md b/.changeset/real-walls-glow.md new file mode 100644 index 0000000000..277c39ca9f --- /dev/null +++ b/.changeset/real-walls-glow.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Don't cache unexpected API errors for more than a few minutes. diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index d36c2445f5..8e6535331a 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -9,7 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { cache } from '../cache'; -import { DataFetcherError, wrapDataFetcherError } from './errors'; +import { DataFetcherError, wrapCacheDataFetcherError } from './errors'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -206,8 +206,8 @@ export function createDataFetcher( const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => { 'use cache'; - return trace(`getUserById(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getUserById(${params.userId})`, async () => { const api = apiClient(input); const res = await api.users.getUserById(params.userId, { ...noCacheFetchOptions, @@ -229,8 +229,8 @@ const getSpace = cache( }) ); - return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { const api = apiClient(input); const res = await api.spaces.getSpaceById( params.spaceId, @@ -260,20 +260,23 @@ const getChangeRequest = cache( }) ); - return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - return res.data; - }); + return wrapCacheDataFetcherError(async () => { + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, + async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + } + ); }); } ); @@ -284,8 +287,8 @@ const getRevision = cache( params: { spaceId: string; revisionId: string; metadata: boolean } ) => { 'use cache'; - return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { const api = apiClient(input); const res = await api.spaces.getRevisionById( params.spaceId, @@ -311,8 +314,8 @@ const getRevisionPages = cache( params: { spaceId: string; revisionId: string; metadata: boolean } ) => { 'use cache'; - return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { const api = apiClient(input); const res = await api.spaces.listPagesInRevisionById( params.spaceId, @@ -338,10 +341,10 @@ const getRevisionFile = cache( params: { spaceId: string; revisionId: string; fileId: string } ) => { 'use cache'; - return trace( - `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getFileInRevisionById( params.spaceId, @@ -355,9 +358,9 @@ const getRevisionFile = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -367,10 +370,10 @@ const getRevisionPageMarkdown = cache( params: { spaceId: string; revisionId: string; pageId: string } ) => { 'use cache'; - return trace( - `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getPageInRevisionById( params.spaceId, @@ -391,9 +394,9 @@ const getRevisionPageMarkdown = cache( throw new DataFetcherError('Page is not a document', 404); } return res.data.markdown; - }); - } - ); + } + ); + }); } ); @@ -403,10 +406,10 @@ const getRevisionPageDocument = cache( params: { spaceId: string; revisionId: string; pageId: string } ) => { 'use cache'; - return trace( - `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getPageDocumentInRevisionById( params.spaceId, @@ -424,9 +427,9 @@ const getRevisionPageDocument = cache( cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -436,11 +439,11 @@ const getRevisionPageByPath = cache( params: { spaceId: string; revisionId: string; path: string } ) => { 'use cache'; - return trace( - `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); const api = apiClient(input); const res = await api.spaces.getPageInRevisionByPath( params.spaceId, @@ -454,17 +457,17 @@ const getRevisionPageByPath = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); const getDocument = cache( async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { 'use cache'; - return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { const api = apiClient(input); const res = await api.spaces.getDocumentById( params.spaceId, @@ -503,10 +506,10 @@ const getComputedDocument = cache( ) ); - return trace( - `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { const api = apiClient(input); const res = await api.spaces.getComputedDocument( params.spaceId, @@ -522,9 +525,9 @@ const getComputedDocument = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -534,10 +537,10 @@ const getReusableContent = cache( params: { spaceId: string; revisionId: string; reusableContentId: string } ) => { 'use cache'; - return trace( - `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getReusableContentInRevisionById( params.spaceId, @@ -551,9 +554,9 @@ const getReusableContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -568,10 +571,10 @@ const getLatestOpenAPISpecVersionContent = cache( }) ); - return trace( - `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { const api = apiClient(input); const res = await api.orgs.getLatestOpenApiSpecVersionContent( params.organizationId, @@ -583,9 +586,9 @@ const getLatestOpenAPISpecVersionContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -602,10 +605,10 @@ const getPublishedContentSite = cache( }) ); - return trace( - `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { const api = apiClient(input); const res = await api.orgs.getPublishedContentSite( params.organizationId, @@ -620,9 +623,9 @@ const getPublishedContentSite = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('days'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -644,10 +647,10 @@ const getSiteRedirectBySource = cache( }) ); - return trace( - `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { const api = apiClient(input); const res = await api.orgs.getSiteRedirectBySource( params.organizationId, @@ -663,9 +666,9 @@ const getSiteRedirectBySource = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('days'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -679,8 +682,8 @@ const getEmbedByUrl = cache( }) ); - return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { const api = apiClient(input); const res = await api.spaces.getEmbedByUrlInSpace( params.spaceId, @@ -712,10 +715,10 @@ const searchSiteContent = cache( }) ); - return trace( - `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { const { organizationId, siteId, query, scope } = params; const api = apiClient(input); const res = await api.orgs.searchSiteContent( @@ -733,9 +736,9 @@ const searchSiteContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('hours'); return res.data.items; - }); - } - ); + } + ); + }); } ); @@ -752,8 +755,8 @@ const renderIntegrationUi = cache( }) ); - return trace(`renderIntegrationUi(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { const api = apiClient(input); const res = await api.integrations.renderIntegrationUiWithPost( params.integrationName, diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts index 4059fb5c94..c4d983ec2a 100644 --- a/packages/gitbook-v2/src/lib/data/errors.ts +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -1,4 +1,5 @@ import { GitBookAPIError } from '@gitbook/api'; +import { unstable_cacheLife as cacheLife } from 'next/cache'; import type { DataFetcherErrorData, DataFetcherResponse } from './types'; export class DataFetcherError extends Error { @@ -89,6 +90,22 @@ export async function wrapDataFetcherError<T>( } } +/** + * Wrap an async execution to handle errors and return a DataFetcherResponse. + * This should be used inside 'use cache' functions. + */ +export async function wrapCacheDataFetcherError<T>( + fn: () => Promise<T> +): Promise<DataFetcherResponse<T>> { + const result = await wrapDataFetcherError(fn); + if (result.error && result.error.code >= 500) { + // We don't want to cache errors for too long. + // as the API might + cacheLife('minutes'); + } + return result; +} + /** * Ignore some data fetcher errors. */ From 6859f7d2391482323667c170b5fb144d1b2fd49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 00:31:07 +0200 Subject: [PATCH 116/127] Fix rendering of ogimage when logo or icon are AVIF images (#3336) Co-authored-by: Steven H <steven@gitbook.io> --- .changeset/pretty-balloons-fold.md | 5 ++ packages/gitbook/src/routes/ogimage.tsx | 80 ++++++++++++++++++------- 2 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 .changeset/pretty-balloons-fold.md diff --git a/.changeset/pretty-balloons-fold.md b/.changeset/pretty-balloons-fold.md new file mode 100644 index 0000000000..b501879843 --- /dev/null +++ b/.changeset/pretty-balloons-fold.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix rendering of ogimage when logo or icon are AVIF images. diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 653fc8fc3b..4035f54bef 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -8,6 +8,7 @@ import { ImageResponse } from 'next/og'; import { type PageParams, fetchPageData } from '@/components/SitePage'; import { getFontSourcesToPreload } from '@/fonts/custom'; import { getAssetURL } from '@/lib/assets'; +import { getExtension } from '@/lib/paths'; import { filterOutNullable } from '@/lib/typescript'; import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; @@ -32,7 +33,6 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page } // Compute all text to load only the necessary fonts - const contentTitle = customization.header.logo ? '' : site.title; const pageTitle = page ? page.title.length > 64 ? `${page.title.slice(0, 64)}...` @@ -52,7 +52,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page const fontFamily = customization.styling.font ?? CustomizationDefaultFont.Inter; const regularText = pageDescription; - const boldText = `${contentTitle}${pageTitle}`; + const boldText = `${site.title} ${pageTitle}`; const fonts = ( await Promise.all([ @@ -164,10 +164,28 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page ) ) ); + if (!iconImage) { + throw new Error('Icon image should always be fetchable'); + } + return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />; }; - const [favicon, { fontFamily, fonts }] = await Promise.all([faviconLoader(), fontLoader()]); + const logoLoader = async () => { + if (!customization.header.logo) { + return null; + } + + return await fetchImage( + useLightTheme ? customization.header.logo.light : customization.header.logo.dark + ); + }; + + const [favicon, logo, { fontFamily, fonts }] = await Promise.all([ + faviconLoader(), + logoLoader(), + fontLoader(), + ]); return new ImageResponse( <div @@ -193,22 +211,14 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page /> {/* Logo */} - {customization.header.logo ? ( + {logo ? ( <div tw="flex flex-row"> - <img - {...(await fetchImage( - useLightTheme - ? customization.header.logo.light - : customization.header.logo.dark - ))} - alt="Logo" - tw="h-[60px]" - /> + <img {...logo} alt="Logo" tw="h-[60px]" /> </div> ) : ( <div tw="flex"> {favicon} - <h3 tw="text-4xl my-0 font-bold">{contentTitle}</h3> + <h3 tw="text-4xl my-0 font-bold">{site.title}</h3> </div> )} @@ -295,7 +305,9 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { // biome-ignore lint/suspicious/noExplicitAny: <explanation> const staticCache = new Map<string, any>(); -// Do we need to limit the in-memory cache size? I think given the usage, we should be fine. +/** + * Get or initialize a value in the static cache. + */ async function getWithCache<T>(key: string, fn: () => Promise<T>) { const cached = staticCache.get(key) as T; if (cached) { @@ -311,19 +323,46 @@ async function getWithCache<T>(key: string, fn: () => Promise<T>) { * Read a static image and cache it in memory. */ async function fetchStaticImage(url: string) { - return getWithCache(`static-image:${url}`, () => fetchImage(url)); + return getWithCache(`static-image:${url}`, async () => { + const image = await fetchImage(url); + if (!image) { + throw new Error('Failed to fetch static image'); + } + + return image; + }); } +/** + * @vercel/og supports the following image formats: + * Extracted from https://github.com/vercel/next.js/blob/canary/packages/next/src/compiled/%40vercel/og/index.node.js + */ +const UNSUPPORTED_IMAGE_EXTENSIONS = ['.avif', '.webp']; +const SUPPORTED_IMAGE_TYPES = [ + 'image/png', + 'image/apng', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', +]; + /** * Fetch an image from a URL and return a base64 encoded string. * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL. */ async function fetchImage(url: string) { + // Skip early some images to avoid fetching them + const parsedURL = new URL(url); + if (UNSUPPORTED_IMAGE_EXTENSIONS.includes(getExtension(parsedURL.pathname).toLowerCase())) { + return null; + } + const response = await fetch(url); + // Filter out unsupported image types const contentType = response.headers.get('content-type'); - if (!contentType || !contentType.startsWith('image/')) { - throw new Error(`Invalid content type: ${contentType}`); + if (!contentType || !SUPPORTED_IMAGE_TYPES.some((type) => contentType.includes(type))) { + return null; } const arrayBuffer = await response.arrayBuffer(); @@ -334,8 +373,7 @@ async function fetchImage(url: string) { try { const { width, height } = imageSize(buffer); return { src, width, height }; - } catch (error) { - console.error(`Error reading image size: ${error}`); - return { src }; + } catch { + return null; } } From 500c8cb649018c639c013a1c4850aaf000528122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 13:04:50 +0200 Subject: [PATCH 117/127] Don't crash ogimage generation on RTL text (#3341) --- .changeset/pink-windows-wonder.md | 5 +++++ bun.lock | 3 +++ packages/gitbook/package.json | 3 ++- packages/gitbook/src/routes/ogimage.tsx | 23 ++++++++++++++++++++--- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 .changeset/pink-windows-wonder.md diff --git a/.changeset/pink-windows-wonder.md b/.changeset/pink-windows-wonder.md new file mode 100644 index 0000000000..d8e9560dcc --- /dev/null +++ b/.changeset/pink-windows-wonder.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Don't crash ogimage generation on RTL text, as a workaround until we can support it. diff --git a/bun.lock b/bun.lock index 94b4fc3a54..8afc0e702e 100644 --- a/bun.lock +++ b/bun.lock @@ -85,6 +85,7 @@ "assert-never": "^1.2.1", "bun-types": "^1.1.20", "classnames": "^2.5.1", + "direction": "^2.0.1", "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", "image-size": "^2.0.2", @@ -1708,6 +1709,8 @@ "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="], + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 2be76259d9..e549013162 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -82,7 +82,8 @@ "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zustand": "^5.0.3", - "image-size": "^2.0.2" + "image-size": "^2.0.2", + "direction": "^2.0.1" }, "devDependencies": { "@argos-ci/playwright": "^5.0.3", diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 4035f54bef..6ce776c699 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -1,6 +1,7 @@ import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api'; import { colorContrast } from '@gitbook/colors'; import { type FontWeight, getDefaultFont } from '@gitbook/fonts'; +import { direction } from 'direction'; import { imageSize } from 'image-size'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; @@ -218,7 +219,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page ) : ( <div tw="flex"> {favicon} - <h3 tw="text-4xl my-0 font-bold">{site.title}</h3> + <h3 tw="text-4xl my-0 font-bold">{transformText(site.title)}</h3> </div> )} @@ -227,10 +228,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page <h1 tw={`text-8xl my-0 tracking-tight leading-none text-left text-[${colors.title}] font-bold`} > - {pageTitle} + {transformText(pageTitle)} </h1> {pageDescription ? ( - <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal">{pageDescription}</h2> + <h2 tw="text-4xl mb-0 mt-8 w-[75%] font-normal"> + {transformText(pageDescription)} + </h2> ) : null} </div> </div>, @@ -377,3 +380,17 @@ async function fetchImage(url: string) { return null; } } + +/** + * @vercel/og doesn't support RTL text, so we need to transform with a HACK for now. + * We can remove it once support has been added. + * https://github.com/vercel/satori/issues/74 + */ +function transformText(text: string) { + const dir = direction(text); + if (dir !== 'rtl') { + return text; + } + + return ''; +} From ff6d1150a5930ffd79144e05882dd9c5cea78684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 16:12:58 +0200 Subject: [PATCH 118/127] Use `getRevisionPageDocument` on v2 (#3342) --- packages/gitbook-v2/src/lib/data/pages.ts | 35 ++--------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts index b9e3cabc5e..319c0a9170 100644 --- a/packages/gitbook-v2/src/lib/data/pages.ts +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -1,4 +1,4 @@ -import { waitUntil } from '@/lib/waitUntil'; +import { isV2 } from '@/lib/v2'; import type { JSONDocument, RevisionPageDocument } from '@gitbook/api'; import type { GitBookSiteContext, GitBookSpaceContext } from '../context'; import { getDataOrNull } from './errors'; @@ -12,10 +12,7 @@ export async function getPageDocument( ): Promise<JSONDocument | null> { const { dataFetcher, space } = context; - if ( - 'site' in context && - (context.site.id === 'site_JOVzv' || context.site.id === 'site_IxAYj') - ) { + if (isV2()) { return getDataOrNull( dataFetcher.getRevisionPageDocument({ spaceId: space.id, @@ -41,33 +38,5 @@ export async function getPageDocument( ); } - // Pre-fetch the document to start filling the cache before we migrate to this API. - if (process.env.NODE_ENV === 'development') { - // Disable for now to investigate side-effects - if (isInPercentRollout(space.id, 10) || process.env.VERCEL_ENV === 'preview') { - await waitUntil( - getDataOrNull( - dataFetcher.getRevisionPageDocument({ - spaceId: space.id, - revisionId: space.revision, - pageId: page.id, - }) - ) - ); - } - } - return null; } - -function isInPercentRollout(value: string, rollout: number) { - return getRandomPercent(value) < rollout; -} - -function getRandomPercent(value: string) { - const hash = value.split('').reduce((acc, char) => { - return acc + char.charCodeAt(0); - }, 0); - - return hash % 100; -} From 67998b6f15b6977c7cc20dafe6a97f3156e95ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 19:10:15 +0200 Subject: [PATCH 119/127] Fix ogimage generation failing with some JPEG images (#3345) --- .changeset/rare-pens-whisper.md | 5 ++++ .../src/lib/images/resizer/types.ts | 2 +- packages/gitbook/src/routes/ogimage.tsx | 25 ++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 .changeset/rare-pens-whisper.md diff --git a/.changeset/rare-pens-whisper.md b/.changeset/rare-pens-whisper.md new file mode 100644 index 0000000000..aed0712ba4 --- /dev/null +++ b/.changeset/rare-pens-whisper.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix ogimage generation failing with some JPEG images. diff --git a/packages/gitbook-v2/src/lib/images/resizer/types.ts b/packages/gitbook-v2/src/lib/images/resizer/types.ts index 8bbe697f90..9cb370586f 100644 --- a/packages/gitbook-v2/src/lib/images/resizer/types.ts +++ b/packages/gitbook-v2/src/lib/images/resizer/types.ts @@ -13,7 +13,7 @@ export interface CloudflareImageJsonFormat { * https://developers.cloudflare.com/images/image-resizing/resize-with-workers/ */ export interface CloudflareImageOptions { - format?: 'webp' | 'avif' | 'json' | 'jpeg'; + format?: 'webp' | 'avif' | 'json' | 'jpeg' | 'png'; fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; width?: number; height?: number; diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 6ce776c699..ce3f8a7d67 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -13,7 +13,13 @@ import { getExtension } from '@/lib/paths'; import { filterOutNullable } from '@/lib/typescript'; import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; -import { getResizedImageURL } from '@v2/lib/images'; +import { + type ResizeImageOptions, + SizableImageAction, + checkIsSizableImageURL, + getResizedImageURL, + resizeImage, +} from '@v2/lib/images'; /** * Render the OpenGraph image for a site content. @@ -178,7 +184,10 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page } return await fetchImage( - useLightTheme ? customization.header.logo.light : customization.header.logo.dark + useLightTheme ? customization.header.logo.light : customization.header.logo.dark, + { + height: 60, + } ); }; @@ -353,14 +362,22 @@ const SUPPORTED_IMAGE_TYPES = [ * Fetch an image from a URL and return a base64 encoded string. * We do this as @vercel/og is otherwise failing on SVG images referenced by a URL. */ -async function fetchImage(url: string) { +async function fetchImage(url: string, options?: ResizeImageOptions) { // Skip early some images to avoid fetching them const parsedURL = new URL(url); if (UNSUPPORTED_IMAGE_EXTENSIONS.includes(getExtension(parsedURL.pathname).toLowerCase())) { return null; } - const response = await fetch(url); + // We use the image resizer to normalize the image format to PNG. + // as @vercel/og can sometimes fail on some JPEG images. + const response = + checkIsSizableImageURL(url) !== SizableImageAction.Resize + ? await fetch(url) + : await resizeImage(url, { + ...options, + format: 'png', + }); // Filter out unsupported image types const contentType = response.headers.get('content-type'); From 88a35ed057935489519086295379eb2aef065cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 19:23:52 +0200 Subject: [PATCH 120/127] Fix crash when integration is triggering invalid requests (#3346) --- .changeset/khaki-bees-count.md | 5 +++++ .../src/components/DocumentView/Integration/render.ts | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/khaki-bees-count.md diff --git a/.changeset/khaki-bees-count.md b/.changeset/khaki-bees-count.md new file mode 100644 index 0000000000..a481690e24 --- /dev/null +++ b/.changeset/khaki-bees-count.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix crash when integration is triggering invalid requests. diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts index a9a86c9e59..4e683e0dcd 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/render.ts +++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts @@ -21,10 +21,13 @@ export async function fetchSafeIntegrationUI( request, }), - // The API can respond with a 400 error if the integration is not installed - // and 404 if the integration is not found. - // The API can also respond with a 502 error if the integration is not generating a proper response. - [404, 400, 502] + // The API can respond with certain errors that are expected to happen. + [ + 404, // Integration has been uninstalled + 400, // Integration is rejecting its own request + 422, // Integration is triggering an invalid request, failing at the validation step + 502, // Integration is failing in an unexpected way + ] ); return output; From a3a944d7ddde5290fe9f070c0461c5884c75db49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 19:38:47 +0200 Subject: [PATCH 121/127] Fix crash during rendering of ogimage for VA sites with default icon (#3347) --- .changeset/thick-cups-shout.md | 5 +++ packages/gitbook/src/routes/icon.tsx | 53 +++++++++++++++++-------- packages/gitbook/src/routes/ogimage.tsx | 51 +++++++++++++----------- 3 files changed, 69 insertions(+), 40 deletions(-) create mode 100644 .changeset/thick-cups-shout.md diff --git a/.changeset/thick-cups-shout.md b/.changeset/thick-cups-shout.md new file mode 100644 index 0000000000..3aabb9ad40 --- /dev/null +++ b/.changeset/thick-cups-shout.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix crash during rendering of ogimage for VA sites with default icon. diff --git a/packages/gitbook/src/routes/icon.tsx b/packages/gitbook/src/routes/icon.tsx index 03a6f35224..ab3429d631 100644 --- a/packages/gitbook/src/routes/icon.tsx +++ b/packages/gitbook/src/routes/icon.tsx @@ -24,6 +24,11 @@ const SIZES = { }, }; +type RenderIconOptions = { + size: keyof typeof SIZES; + theme: 'light' | 'dark'; +}; + /** * Generate an icon for a site content. */ @@ -31,7 +36,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) { const options = getOptions(req.url); const size = SIZES[options.size]; - const { site, customization } = context; + const { customization } = context; const customIcon = 'icon' in customization.favicon ? customization.favicon.icon : null; // If the site has a custom icon, redirect to it @@ -45,17 +50,45 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) { ); } + return new ImageResponse(<SiteDefaultIcon context={context} options={options} />, { + width: size.width, + height: size.height, + headers: { + 'cache-tag': [ + getCacheTag({ + tag: 'site', + site: context.site.id, + }), + ].join(','), + }, + }); +} + +/** + * Render the icon as a React node. + */ +export function SiteDefaultIcon(props: { + context: GitBookSiteContext; + options: RenderIconOptions; + style?: React.CSSProperties; + tw?: string; +}) { + const { context, options, style, tw } = props; + const size = SIZES[options.size]; + + const { site, customization } = context; const contentTitle = site.title; - return new ImageResponse( + return ( <div - tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle)} + tw={tcls(options.theme === 'light' ? 'bg-white' : 'bg-black', size.boxStyle, tw)} style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', + ...style, }} > <h2 @@ -70,19 +103,7 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) { ? getEmojiForCode(customization.favicon.emoji) : contentTitle.slice(0, 1).toUpperCase()} </h2> - </div>, - { - width: size.width, - height: size.height, - headers: { - 'cache-tag': [ - getCacheTag({ - tag: 'site', - site: context.site.id, - }), - ].join(','), - }, - } + </div> ); } diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index ce3f8a7d67..f79b86948f 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -20,13 +20,14 @@ import { getResizedImageURL, resizeImage, } from '@v2/lib/images'; +import { SiteDefaultIcon } from './icon'; /** * Render the OpenGraph image for a site content. */ export async function serveOGImage(baseContext: GitBookSiteContext, params: PageParams) { const { context, pageTarget } = await fetchPageData(baseContext, params); - const { customization, site, linker, imageResizer } = context; + const { customization, site, imageResizer } = context; const page = pageTarget?.page; // If user configured a custom social preview, we redirect to it. @@ -148,34 +149,36 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page } const faviconLoader = async () => { + if (customization.header.logo) { + // Don't load the favicon if we have a logo + // as it'll not be used. + return null; + } + + const faviconSize = { + width: 48, + height: 48, + }; + if ('icon' in customization.favicon) return ( <img - src={customization.favicon.icon[theme]} - width={40} - height={40} - tw="mr-4" + {...(await fetchImage(customization.favicon.icon[theme], faviconSize))} + {...faviconSize} alt="Icon" /> ); - if ('emoji' in customization.favicon) - return ( - <span tw="text-4xl mr-4"> - {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))} - </span> - ); - const iconImage = await fetchImage( - linker.toAbsoluteURL( - linker.toPathInSpace( - `~gitbook/icon?size=medium&theme=${customization.themes.default}` - ) - ) - ); - if (!iconImage) { - throw new Error('Icon image should always be fetchable'); - } - return <img {...iconImage} alt="Icon" width={40} height={40} tw="mr-4" />; + return ( + <SiteDefaultIcon + context={context} + options={{ + size: 'small', + theme, + }} + style={faviconSize} + /> + ); }; const logoLoader = async () => { @@ -226,9 +229,9 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page <img {...logo} alt="Logo" tw="h-[60px]" /> </div> ) : ( - <div tw="flex"> + <div tw="flex flex-row items-center"> {favicon} - <h3 tw="text-4xl my-0 font-bold">{transformText(site.title)}</h3> + <h3 tw="text-4xl ml-4 my-0 font-bold">{transformText(site.title)}</h3> </div> )} From 8f7c304d5852ecdeb556085f211c4369bd861b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= <samypesse@gmail.com> Date: Tue, 17 Jun 2025 20:25:21 +0200 Subject: [PATCH 122/127] Fix crash when rendering ogimage with invalid icon (#3348) --- packages/gitbook/src/routes/ogimage.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index f79b86948f..4d8a83b2c3 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -160,14 +160,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page height: 48, }; - if ('icon' in customization.favicon) - return ( - <img - {...(await fetchImage(customization.favicon.icon[theme], faviconSize))} - {...faviconSize} - alt="Icon" - /> - ); + if ('icon' in customization.favicon) { + const faviconImage = await fetchImage(customization.favicon.icon[theme], faviconSize); + if (faviconImage) { + return <img {...faviconImage} {...faviconSize} alt="Icon" />; + } + } return ( <SiteDefaultIcon From b4918f60cebccb4660a2a32a738f513eba83f0e3 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Tue, 17 Jun 2025 22:31:32 +0200 Subject: [PATCH 123/127] fix variant space change when pathname equals space-path (#3343) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../src/components/Header/SpacesDropdown.tsx | 21 ++++----- .../Header/SpacesDropdownMenuItem.tsx | 45 +++++++++++++++++-- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index dd6887d955..87ad44b5d7 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -4,7 +4,7 @@ import { getSiteSpaceURL } from '@/lib/sites'; import { tcls } from '@/lib/tailwind'; import type { GitBookSiteContext } from '@v2/lib/context'; import { DropdownChevron, DropdownMenu } from './DropdownMenu'; -import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem'; +import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem'; export function SpacesDropdown(props: { context: GitBookSiteContext; @@ -65,17 +65,14 @@ export function SpacesDropdown(props: { </div> } > - {siteSpaces.map((otherSiteSpace, index) => ( - <SpacesDropdownMenuItem - key={`${otherSiteSpace.id}-${index}`} - variantSpace={{ - id: otherSiteSpace.id, - title: otherSiteSpace.title, - url: getSiteSpaceURL(context, otherSiteSpace), - }} - active={otherSiteSpace.id === siteSpace.id} - /> - ))} + <SpacesDropdownMenuItems + slimSpaces={siteSpaces.map((space) => ({ + id: space.id, + title: space.title, + url: getSiteSpaceURL(context, space), + }))} + curPath={siteSpace.path} + /> </DropdownMenu> ); } diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx index 70859abfae..2044d0fc6f 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx @@ -6,12 +6,28 @@ import { joinPath } from '@/lib/paths'; import { useCurrentPagePath } from '../hooks'; import { DropdownMenuItem } from './DropdownMenu'; -function useVariantSpaceHref(variantSpaceUrl: string) { +interface VariantSpace { + id: Space['id']; + title: Space['title']; + url: string; +} + +// When switching to a different variant space, we reconstruct the URL by swapping the space path. +function useVariantSpaceHref(variantSpaceUrl: string, currentSpacePath: string, active = false) { const currentPathname = useCurrentPagePath(); + // We need to ensure that the variant space URL is not the same as the current space path. + // If it is, we return only the variant space URL to redirect to the root of the variant space. + // This is necessary in case the currentPathname is the same as the variantSpaceUrl, + // otherwise we would redirect to the same space if the variant space that we are switching to is the default one. + if (!active && currentPathname.startsWith(`${currentSpacePath}/`)) { + return variantSpaceUrl; + } + if (URL.canParse(variantSpaceUrl)) { const targetUrl = new URL(variantSpaceUrl); targetUrl.pathname = joinPath(targetUrl.pathname, currentPathname); + targetUrl.searchParams.set('fallback', 'true'); return targetUrl.toString(); @@ -22,11 +38,12 @@ function useVariantSpaceHref(variantSpaceUrl: string) { } export function SpacesDropdownMenuItem(props: { - variantSpace: { id: Space['id']; title: Space['title']; url: string }; + variantSpace: VariantSpace; active: boolean; + currentSpacePath: string; }) { - const { variantSpace, active } = props; - const variantHref = useVariantSpaceHref(variantSpace.url); + const { variantSpace, active, currentSpacePath } = props; + const variantHref = useVariantSpaceHref(variantSpace.url, currentSpacePath, active); return ( <DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}> @@ -34,3 +51,23 @@ export function SpacesDropdownMenuItem(props: { </DropdownMenuItem> ); } + +export function SpacesDropdownMenuItems(props: { + slimSpaces: VariantSpace[]; + curPath: string; +}) { + const { slimSpaces, curPath } = props; + + return ( + <> + {slimSpaces.map((space) => ( + <SpacesDropdownMenuItem + key={space.id} + variantSpace={space} + active={false} + currentSpacePath={curPath} + /> + ))} + </> + ); +} From 392f59450caee52d07bc6b59a0c2934df1cd23e1 Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Wed, 18 Jun 2025 12:15:45 +0100 Subject: [PATCH 124/127] Improve performance of InlineLinkTooltip (#3339) --- .changeset/soft-walls-change.md | 6 + .../components/DocumentView/DocumentView.tsx | 8 + .../components/DocumentView/InlineLink.tsx | 61 ------- .../DocumentView/InlineLink/InlineLink.tsx | 153 ++++++++++++++++++ .../InlineLink/InlineLinkTooltip.tsx | 73 +++++++++ .../InlineLinkTooltipImpl.tsx} | 101 ++++-------- .../DocumentView/InlineLink/index.ts | 1 + .../gitbook/src/components/PDF/PDFPage.tsx | 1 + .../src/components/PageBody/PageBody.tsx | 14 +- .../src/components/Search/server-actions.tsx | 1 + packages/gitbook/src/lib/document.ts | 35 ++++ 11 files changed, 325 insertions(+), 129 deletions(-) create mode 100644 .changeset/soft-walls-change.md delete mode 100644 packages/gitbook/src/components/DocumentView/InlineLink.tsx create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx rename packages/gitbook/src/components/DocumentView/{InlineLinkTooltip.tsx => InlineLink/InlineLinkTooltipImpl.tsx} (64%) create mode 100644 packages/gitbook/src/components/DocumentView/InlineLink/index.ts diff --git a/.changeset/soft-walls-change.md b/.changeset/soft-walls-change.md new file mode 100644 index 0000000000..2086de0317 --- /dev/null +++ b/.changeset/soft-walls-change.md @@ -0,0 +1,6 @@ +--- +"gitbook": patch +"gitbook-v2": patch +--- + +Fix InlineLinkTooltip having a negative impact on performance, especially on larger pages. diff --git a/packages/gitbook/src/components/DocumentView/DocumentView.tsx b/packages/gitbook/src/components/DocumentView/DocumentView.tsx index 724a7f2b05..1ff8542eed 100644 --- a/packages/gitbook/src/components/DocumentView/DocumentView.tsx +++ b/packages/gitbook/src/components/DocumentView/DocumentView.tsx @@ -28,6 +28,14 @@ export interface DocumentContext { * @default true */ wrapBlocksInSuspense?: boolean; + + /** + * True if link previews should be rendered. + * This is used to limit the number of link previews rendered in a document. + * If false, no link previews will be rendered. + * @default false + */ + shouldRenderLinkPreviews?: boolean; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink.tsx deleted file mode 100644 index f5bd7f798b..0000000000 --- a/packages/gitbook/src/components/DocumentView/InlineLink.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api'; - -import { resolveContentRef } from '@/lib/references'; -import { Icon } from '@gitbook/icons'; -import { StyledLink } from '../primitives'; -import type { InlineProps } from './Inline'; -import { InlineLinkTooltip } from './InlineLinkTooltip'; -import { Inlines } from './Inlines'; - -export async function InlineLink(props: InlineProps<DocumentInlineLink>) { - const { inline, document, context, ancestorInlines } = props; - - const resolved = context.contentContext - ? await resolveContentRef(inline.data.ref, context.contentContext, { - // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link. - resolveAnchorText: false, - }) - : null; - - if (!context.contentContext || !resolved) { - return ( - <span title="Broken link" className="underline"> - <Inlines - context={context} - document={document} - nodes={inline.nodes} - ancestorInlines={[...ancestorInlines, inline]} - /> - </span> - ); - } - const isExternal = inline.data.ref.kind === 'url'; - - return ( - <InlineLinkTooltip inline={inline} context={context.contentContext} resolved={resolved}> - <StyledLink - href={resolved.href} - insights={{ - type: 'link_click', - link: { - target: inline.data.ref, - position: SiteInsightsLinkPosition.Content, - }, - }} - > - <Inlines - context={context} - document={document} - nodes={inline.nodes} - ancestorInlines={[...ancestorInlines, inline]} - /> - {isExternal ? ( - <Icon - icon="arrow-up-right" - className="ml-0.5 inline size-3 links-accent:text-tint-subtle" - /> - ) : null} - </StyledLink> - </InlineLinkTooltip> - ); -} diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx new file mode 100644 index 0000000000..8ad287a351 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLink.tsx @@ -0,0 +1,153 @@ +import { type DocumentInlineLink, SiteInsightsLinkPosition } from '@gitbook/api'; + +import { getSpaceLanguage, tString } from '@/intl/server'; +import { languages } from '@/intl/translations'; +import { type ResolvedContentRef, resolveContentRef } from '@/lib/references'; +import { Icon } from '@gitbook/icons'; +import type { GitBookAnyContext } from '@v2/lib/context'; +import { StyledLink } from '../../primitives'; +import type { InlineProps } from '../Inline'; +import { Inlines } from '../Inlines'; +import { InlineLinkTooltip } from './InlineLinkTooltip'; + +export async function InlineLink(props: InlineProps<DocumentInlineLink>) { + const { inline, document, context, ancestorInlines } = props; + + const resolved = context.contentContext + ? await resolveContentRef(inline.data.ref, context.contentContext, { + // We don't want to resolve the anchor text here, as it can be very expensive and will block rendering if there is a lot of anchors link. + resolveAnchorText: false, + }) + : null; + + if (!context.contentContext || !resolved) { + return ( + <span title="Broken link" className="underline"> + <Inlines + context={context} + document={document} + nodes={inline.nodes} + ancestorInlines={[...ancestorInlines, inline]} + /> + </span> + ); + } + const isExternal = inline.data.ref.kind === 'url'; + const content = ( + <StyledLink + href={resolved.href} + insights={{ + type: 'link_click', + link: { + target: inline.data.ref, + position: SiteInsightsLinkPosition.Content, + }, + }} + > + <Inlines + context={context} + document={document} + nodes={inline.nodes} + ancestorInlines={[...ancestorInlines, inline]} + /> + {isExternal ? ( + <Icon + icon="arrow-up-right" + className="ml-0.5 inline size-3 links-accent:text-tint-subtle" + /> + ) : null} + </StyledLink> + ); + + if (context.shouldRenderLinkPreviews) { + return ( + <InlineLinkTooltipWrapper + inline={inline} + context={context.contentContext} + resolved={resolved} + > + {content} + </InlineLinkTooltipWrapper> + ); + } + + return content; +} + +/** + * An SSR component that renders a link with a tooltip. + * Essentially it pulls the minimum amount of props from the context to render the tooltip. + */ +function InlineLinkTooltipWrapper(props: { + inline: DocumentInlineLink; + context: GitBookAnyContext; + children: React.ReactNode; + resolved: ResolvedContentRef; +}) { + const { inline, context, resolved, children } = props; + + let breadcrumbs = resolved.ancestors ?? []; + const language = + 'customization' in context ? getSpaceLanguage(context.customization) : languages.en; + const isExternal = inline.data.ref.kind === 'url'; + const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined; + if (isExternal) { + breadcrumbs = [ + { + label: tString(language, 'link_tooltip_external_link'), + }, + ]; + } + if (isSamePage) { + breadcrumbs = [ + { + label: tString(language, 'link_tooltip_page_anchor'), + icon: <Icon icon="arrow-down-short-wide" className="size-3" />, + }, + ]; + resolved.subText = undefined; + } + + const aiSummary: { pageId: string; spaceId: string } | undefined = (() => { + if (isExternal) { + return; + } + + if (isSamePage) { + return; + } + + if (!('customization' in context) || !context.customization.ai?.pageLinkSummaries.enabled) { + return; + } + + if (!('page' in context) || !('page' in inline.data.ref)) { + return; + } + + if (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor') { + return { + pageId: resolved.page?.id ?? inline.data.ref.page ?? context.page.id, + spaceId: inline.data.ref.space ?? context.space.id, + }; + } + })(); + + return ( + <InlineLinkTooltip + breadcrumbs={breadcrumbs} + isExternal={isExternal} + isSamePage={isSamePage} + aiSummary={aiSummary} + openInNewTabLabel={tString(language, 'open_in_new_tab')} + target={{ + href: resolved.href, + text: resolved.text, + subText: resolved.subText, + icon: resolved.icon, + }} + > + {children} + </InlineLinkTooltip> + ); +} diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx new file mode 100644 index 0000000000..45be0c7173 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltip.tsx @@ -0,0 +1,73 @@ +'use client'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +const LoadingValueContext = React.createContext<React.ReactNode>(null); + +// To avoid polluting the RSC payload with the tooltip implementation, +// we lazily load it on the client side. This way, the tooltip is only loaded +// when the user interacts with the link, and it doesn't block the initial render. + +const InlineLinkTooltipImpl = dynamic( + () => import('./InlineLinkTooltipImpl').then((mod) => mod.InlineLinkTooltipImpl), + { + // Disable server-side rendering for this component, it's only + // visible on user interaction. + ssr: false, + loading: () => { + // The fallback should be the children (the content of the link), + // but as next/dynamic is aiming for feature parity with React.lazy, + // it doesn't support passing children to the loading component. + // https://github.com/vercel/next.js/issues/7906 + const children = React.useContext(LoadingValueContext); + return <>{children}</>; + }, + } +); + +/** + * Tooltip for inline links. It's lazily loaded to avoid blocking the initial render + * and polluting the RSC payload. + * + * The link text and href have already been rendered on the server for good SEO, + * so we can be as lazy as possible with the tooltip. + */ +export function InlineLinkTooltip(props: { + isSamePage: boolean; + isExternal: boolean; + aiSummary?: { pageId: string; spaceId: string }; + breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>; + target: { + href: string; + text: string; + subText?: string; + icon?: React.ReactNode; + }; + openInNewTabLabel: string; + children: React.ReactNode; +}) { + const { children, ...rest } = props; + const [shouldLoad, setShouldLoad] = React.useState(false); + + // Once the browser is idle, we set shouldLoad to true. + // NOTE: to be slightly more performant, we could load when a link is hovered. + // But I found this was too much of a delay for the tooltip to appear. + // Loading on idle is a good compromise, as it allows the initial render to be fast, + // while still loading the tooltip in the background and not polluting the RSC payload. + React.useEffect(() => { + if ('requestIdleCallback' in window) { + (window as globalThis.Window).requestIdleCallback(() => setShouldLoad(true)); + } else { + // fallback for old browsers + setTimeout(() => setShouldLoad(true), 2000); + } + }, []); + + return shouldLoad ? ( + <LoadingValueContext.Provider value={children}> + <InlineLinkTooltipImpl {...rest}>{children}</InlineLinkTooltipImpl> + </LoadingValueContext.Provider> + ) : ( + children + ); +} diff --git a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx similarity index 64% rename from packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx rename to packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx index 1a5cefa1e3..333184a2cd 100644 --- a/packages/gitbook/src/components/DocumentView/InlineLinkTooltip.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineLink/InlineLinkTooltipImpl.tsx @@ -1,55 +1,27 @@ -import type { DocumentInlineLink } from '@gitbook/api'; - -import type { ResolvedContentRef } from '@/lib/references'; - -import { getSpaceLanguage } from '@/intl/server'; -import { tString } from '@/intl/translate'; -import { languages } from '@/intl/translations'; -import { getNodeText } from '@/lib/document'; +'use client'; import { tcls } from '@/lib/tailwind'; import { Icon } from '@gitbook/icons'; import * as Tooltip from '@radix-ui/react-tooltip'; -import type { GitBookAnyContext } from '@v2/lib/context'; import { Fragment } from 'react'; -import { AIPageLinkSummary } from '../Adaptive/AIPageLinkSummary'; -import { Button, StyledLink } from '../primitives'; +import { AIPageLinkSummary } from '../../Adaptive'; +import { Button, StyledLink } from '../../primitives'; -export async function InlineLinkTooltip(props: { - inline: DocumentInlineLink; - context: GitBookAnyContext; +export function InlineLinkTooltipImpl(props: { + isSamePage: boolean; + isExternal: boolean; + aiSummary?: { pageId: string; spaceId: string }; + breadcrumbs: Array<{ href?: string; label: string; icon?: React.ReactNode }>; + target: { + href: string; + text: string; + subText?: string; + icon?: React.ReactNode; + }; + openInNewTabLabel: string; children: React.ReactNode; - resolved: ResolvedContentRef; }) { - const { inline, context, resolved, children } = props; - - let breadcrumbs = resolved.ancestors; - const language = - 'customization' in context ? getSpaceLanguage(context.customization) : languages.en; - const isExternal = inline.data.ref.kind === 'url'; - const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined; - if (isExternal) { - breadcrumbs = [ - { - label: tString(language, 'link_tooltip_external_link'), - }, - ]; - } - if (isSamePage) { - breadcrumbs = [ - { - label: tString(language, 'link_tooltip_page_anchor'), - icon: <Icon icon="arrow-down-short-wide" className="size-3" />, - }, - ]; - resolved.subText = undefined; - } - - const hasAISummary = - !isExternal && - !isSamePage && - 'customization' in context && - context.customization.ai?.pageLinkSummaries.enabled && - (inline.data.ref.kind === 'page' || inline.data.ref.kind === 'anchor'); + const { isSamePage, isExternal, aiSummary, openInNewTabLabel, target, breadcrumbs, children } = + props; return ( <Tooltip.Provider delayDuration={200}> @@ -100,15 +72,15 @@ export async function InlineLinkTooltip(props: { isExternal && 'text-sm [overflow-wrap:anywhere]' )} > - {resolved.icon ? ( + {target.icon ? ( <div className="mt-1 text-tint-subtle empty:hidden"> - {resolved.icon} + {target.icon} </div> ) : null} - <h5 className="font-semibold">{resolved.text}</h5> + <h5 className="font-semibold">{target.text}</h5> </div> </div> - {!isSamePage && resolved.href ? ( + {!isSamePage && target.href ? ( <Button className={tcls( '-mx-2 -my-2 ml-auto', @@ -117,40 +89,35 @@ export async function InlineLinkTooltip(props: { : null )} variant="blank" - href={resolved.href} + href={target.href} target="_blank" - label={tString(language, 'open_in_new_tab')} + label={openInNewTabLabel} size="small" icon="arrow-up-right-from-square" iconOnly={true} /> ) : null} </div> - {resolved.subText ? ( - <p className="mt-1 text-sm text-tint">{resolved.subText}</p> + {target.subText ? ( + <p className="mt-1 text-sm text-tint">{target.subText}</p> ) : null} </div> - {hasAISummary && 'page' in context && 'page' in inline.data.ref ? ( + {aiSummary ? ( <div className="border-tint-subtle border-t bg-tint p-4"> <AIPageLinkSummary - targetPageId={ - resolved.page?.id ?? - inline.data.ref.page ?? - context.page.id - } - targetSpaceId={inline.data.ref.space ?? context.space.id} - linkTitle={getNodeText(inline)} - linkPreview={`**${resolved.text}**: ${resolved.subText}`} - showTrademark={ - 'customization' in context && - context.customization.trademark.enabled - } + targetPageId={aiSummary.pageId} + targetSpaceId={aiSummary.spaceId} + showTrademark /> </div> ) : null} </div> - <Tooltip.Arrow className={hasAISummary ? 'fill-tint-3' : 'fill-tint-1'} /> + <Tooltip.Arrow + className={ + typeof aiSummary !== 'undefined' ? 'fill-tint-3' : 'fill-tint-1' + } + /> </Tooltip.Content> </Tooltip.Portal> </Tooltip.Root> diff --git a/packages/gitbook/src/components/DocumentView/InlineLink/index.ts b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts new file mode 100644 index 0000000000..1d7f30120b --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineLink/index.ts @@ -0,0 +1 @@ +export * from './InlineLink'; diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index f70b340b41..cc9bf28077 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -245,6 +245,7 @@ async function PDFPageDocument(props: { page, }, getId: (id) => getPagePDFContainerId(page, id), + shouldRenderLinkPreviews: false, // We don't want to render link previews in the PDF. }} // We consider all pages as offscreen in PDF mode // to ensure we can efficiently render as many pages as possible diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 801db03ce2..9b9e744743 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; -import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document'; +import { hasFullWidthBlock, hasMoreThan, isNodeEmpty } from '@/lib/document'; import type { AncestorRevisionPage } from '@/lib/pages'; import { tcls } from '@/lib/tailwind'; import { DocumentView, DocumentViewSkeleton } from '../DocumentView'; @@ -17,6 +17,8 @@ import { PageFooterNavigation } from './PageFooterNavigation'; import { PageHeader } from './PageHeader'; import { PreservePageLayout } from './PreservePageLayout'; +const LINK_PREVIEW_MAX_COUNT = 100; + export function PageBody(props: { context: GitBookSiteContext; page: RevisionPageDocument; @@ -28,6 +30,15 @@ export function PageBody(props: { const { customization } = context; const contentFullWidth = document ? hasFullWidthBlock(document) : false; + + // Render link previews only if there are less than LINK_PREVIEW_MAX_COUNT links in the document. + const shouldRenderLinkPreviews = document + ? !hasMoreThan( + document, + (inline) => inline.object === 'inline' && inline.type === 'link', + LINK_PREVIEW_MAX_COUNT + ) + : false; const pageFullWidth = page.id === 'wtthNFMqmEQmnt5LKR0q'; const asFullWidth = pageFullWidth || contentFullWidth; const language = getSpaceLanguage(customization); @@ -68,6 +79,7 @@ export function PageBody(props: { context={{ mode: 'default', contentContext: context, + shouldRenderLinkPreviews, }} /> </React.Suspense> diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index b45bc0aba1..9bd8257c16 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -345,6 +345,7 @@ async function transformAnswer( mode: 'default', contentContext: undefined, wrapBlocksInSuspense: false, + shouldRenderLinkPreviews: false, // We don't want to render link previews in the AI answer. }} style={['space-y-5']} /> diff --git a/packages/gitbook/src/lib/document.ts b/packages/gitbook/src/lib/document.ts index 33618f76c1..8d9fabc1e5 100644 --- a/packages/gitbook/src/lib/document.ts +++ b/packages/gitbook/src/lib/document.ts @@ -30,6 +30,41 @@ export function hasFullWidthBlock(document: JSONDocument): boolean { return false; } +/** + * Returns true if the document has more than `limit` blocks and/or inlines that match the `check` predicate. + */ +export function hasMoreThan( + document: JSONDocument | DocumentBlock, + check: (block: DocumentBlock | DocumentInline) => boolean, + limit = 1 +): boolean { + let count = 0; + + function traverse(node: JSONDocument | DocumentBlock | DocumentFragment): boolean { + for (const child of 'nodes' in node ? node.nodes : []) { + if (child.object === 'text') continue; + + if (check(child)) { + count++; + if (count > limit) return true; + } + + if (child.object === 'block' && 'nodes' in child) { + if (traverse(child)) return true; + } + + if (child.object === 'block' && 'fragments' in child) { + for (const fragment of child.fragments) { + if (traverse(fragment)) return true; + } + } + } + return false; + } + + return traverse(document); +} + /** * Get the text of a block/inline. */ From 73e0cbb2d659a4236114cd550bbb6cc5efcb61e9 Mon Sep 17 00:00:00 2001 From: Steven H <steven@gitbook.io> Date: Wed, 18 Jun 2025 13:26:20 +0100 Subject: [PATCH 125/127] Fix an issue where PDF export URLs were not bringing their query params. (#3351) --- .changeset/rich-buses-hunt.md | 5 + packages/gitbook-v2/src/middleware.ts | 2 + packages/gitbook/e2e/pdf.spec.ts | 170 ++++++++++++++++++ packages/gitbook/e2e/util.ts | 2 +- packages/gitbook/package.json | 2 +- .../gitbook/src/components/PDF/PDFPage.tsx | 2 +- 6 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 .changeset/rich-buses-hunt.md create mode 100644 packages/gitbook/e2e/pdf.spec.ts diff --git a/.changeset/rich-buses-hunt.md b/.changeset/rich-buses-hunt.md new file mode 100644 index 0000000000..fbc59d232d --- /dev/null +++ b/.changeset/rich-buses-hunt.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Fix an issue where PDF export URLs were not keeping their query params. diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index d413d6a435..4bcbf77aea 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -259,6 +259,8 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { console.log(`rewriting ${request.nextUrl.toString()} to ${route}`); const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString()); + rewrittenURL.search = request.nextUrl.search; // Preserve the original search params + const response = NextResponse.rewrite(rewrittenURL, { request: { headers: requestHeaders, diff --git a/packages/gitbook/e2e/pdf.spec.ts b/packages/gitbook/e2e/pdf.spec.ts new file mode 100644 index 0000000000..4798a2a047 --- /dev/null +++ b/packages/gitbook/e2e/pdf.spec.ts @@ -0,0 +1,170 @@ +import { argosScreenshot } from '@argos-ci/playwright'; +import { expect, test } from '@playwright/test'; +import { getContentTestURL } from '../tests/utils'; +import { waitForIcons } from './util'; + +test.describe('PDF export', () => { + test('export all pages as PDF (e2e)', async ({ page }) => { + // Set the header to disable the Vercel toolbar + // But only on the main document as it'd cause CORS issues on other resources + await page.route('**/*', async (route, request) => { + if (request.resourceType() === 'document') { + await route.continue({ + headers: { + ...request.headers(), + 'x-vercel-skip-toolbar': '1', + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto( + getContentTestURL( + 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?limit=10' + ) + ); + + const printBtn = page.getByTestId('print-button'); + await expect(printBtn).toBeVisible(); + + await argosScreenshot(page, 'pdf - all pages', { + viewports: ['macbook-13'], + argosCSS: ` + /* Hide Intercom */ + .intercom-lightweight-app { + display: none !important; + } + `, + threshold: undefined, + fullPage: true, + beforeScreenshot: async ({ runStabilization }) => { + await runStabilization(); + await waitForIcons(page); + }, + }); + }); + + test('export all pages as PDF (GitBook docs)', async ({ page }) => { + // Set the header to disable the Vercel toolbar + // But only on the main document as it'd cause CORS issues on other resources + await page.route('**/*', async (route, request) => { + if (request.resourceType() === 'document') { + await route.continue({ + headers: { + ...request.headers(), + 'x-vercel-skip-toolbar': '1', + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto(getContentTestURL('https://gitbook.com/docs/~gitbook/pdf?limit=10')); + + const printBtn = page.getByTestId('print-button'); + await expect(printBtn).toBeVisible(); + + await argosScreenshot(page, 'pdf - all pages', { + viewports: ['macbook-13'], + argosCSS: ` + /* Hide Intercom */ + .intercom-lightweight-app { + display: none !important; + } + `, + threshold: undefined, + fullPage: true, + beforeScreenshot: async ({ runStabilization }) => { + await runStabilization(); + await waitForIcons(page); + }, + }); + }); + + test('export a single page as PDF (e2e)', async ({ page }) => { + // Set the header to disable the Vercel toolbar + // But only on the main document as it'd cause CORS issues on other resources + await page.route('**/*', async (route, request) => { + if (request.resourceType() === 'document') { + await route.continue({ + headers: { + ...request.headers(), + 'x-vercel-skip-toolbar': '1', + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto( + getContentTestURL( + 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/~gitbook/pdf?page=Bw7LjWwgTjV8nIV4s7rs&only=yes&limit=2' + ) + ); + + const printBtn = page.getByTestId('print-button'); + await expect(printBtn).toBeVisible(); + + await argosScreenshot(page, 'pdf - all pages', { + viewports: ['macbook-13'], + argosCSS: ` + /* Hide Intercom */ + .intercom-lightweight-app { + display: none !important; + } + `, + threshold: undefined, + fullPage: true, + beforeScreenshot: async ({ runStabilization }) => { + await runStabilization(); + await waitForIcons(page); + }, + }); + }); + + test('export a single page as PDF (GitBook docs)', async ({ page }) => { + // Set the header to disable the Vercel toolbar + // But only on the main document as it'd cause CORS issues on other resources + await page.route('**/*', async (route, request) => { + if (request.resourceType() === 'document') { + await route.continue({ + headers: { + ...request.headers(), + 'x-vercel-skip-toolbar': '1', + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto( + getContentTestURL( + 'https://gitbook.com/docs/~gitbook/pdf?page=DfnNkU49mvLe2ythHAyx&only=yes&limit=2' + ) + ); + + const printBtn = page.getByTestId('print-button'); + await expect(printBtn).toBeVisible(); + + await argosScreenshot(page, 'pdf - all pages', { + viewports: ['macbook-13'], + argosCSS: ` + /* Hide Intercom */ + .intercom-lightweight-app { + display: none !important; + } + `, + threshold: undefined, + fullPage: true, + beforeScreenshot: async ({ runStabilization }) => { + await runStabilization(); + await waitForIcons(page); + }, + }); + }); +}); diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index f54b70fbe2..937b776d70 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -346,7 +346,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin /** * Wait for all icons present on the page to be loaded. */ -async function waitForIcons(page: Page) { +export async function waitForIcons(page: Page) { await page.waitForFunction(() => { const urlStates: Record< string, diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index e549013162..a430218cfb 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -8,7 +8,7 @@ "build:cloudflare": "next-on-pages --custom-entrypoint=./src/cloudflare-entrypoint.ts", "start": "next start", "typecheck": "tsc --noEmit", - "e2e": "playwright test e2e/internal.spec.ts", + "e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts", "e2e-customers": "playwright test e2e/customers.spec.ts", "unit": "bun test {src,packages} --preload ./tests/preload-bun.ts", "generate": "gitbook-icons ./public/~gitbook/static/icons custom-icons && gitbook-math ./public/~gitbook/static/math", diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index cc9bf28077..d2bc5c91a6 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -53,7 +53,7 @@ export async function PDFPage(props: { }) { const baseContext = props.context; const searchParams = new URLSearchParams(props.searchParams); - const pdfParams = getPDFSearchParams(new URLSearchParams(searchParams)); + const pdfParams = getPDFSearchParams(searchParams); const customization = 'customization' in baseContext ? baseContext.customization : defaultCustomization(); From dae019c11501477fcf21313ec8c8ff9b35f6230c Mon Sep 17 00:00:00 2001 From: Zeno Kapitein <zeno@gitbook.io> Date: Wed, 18 Jun 2025 17:48:49 +0200 Subject: [PATCH 126/127] Consistently show variant selector in section bar if site has sections (#3349) --- .changeset/slimy-hornets-share.md | 5 +++++ packages/gitbook/src/components/Header/Header.tsx | 2 +- packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/slimy-hornets-share.md diff --git a/.changeset/slimy-hornets-share.md b/.changeset/slimy-hornets-share.md new file mode 100644 index 0000000000..bfeef22793 --- /dev/null +++ b/.changeset/slimy-hornets-share.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Consistently show variant selector in section bar if site has sections diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 304e853139..f3bd145d56 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -202,7 +202,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo {siteSpaces.length > 1 && ( <div id="variants" - className="my-2 mr-5 page-no-toc:flex hidden grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64" + className="my-2 mr-5 grow border-tint border-r pr-5 *:grow only:mr-0 only:border-none only:pr-0 sm:max-w-64" > <SpacesDropdown context={context} diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index cedc2eee89..f69de405e3 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -125,7 +125,7 @@ export function SpaceLayout(props: { sections={encodeClientSiteSections(context, sections)} /> )} - {isMultiVariants && ( + {isMultiVariants && !sections && ( <SpacesDropdown context={context} siteSpace={siteSpace} From 8d3c6562a266fc24d20a94af7f2f9c1e629970d6 Mon Sep 17 00:00:00 2001 From: conico974 <nicodorseuil@yahoo.fr> Date: Wed, 18 Jun 2025 19:02:32 +0200 Subject: [PATCH 127/127] Implement a StylesProvider (#3352) Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io> --- .../Announcement/AnnouncementBanner.tsx | 5 +- .../DocumentView/Table/RecordCard.tsx | 30 +------- .../components/DocumentView/Table/styles.ts | 26 +++++++ .../TableOfContents/PageLinkItem.tsx | 20 +---- .../TableOfContents/ToggleableLinkItem.tsx | 40 +--------- .../src/components/TableOfContents/styles.ts | 56 ++++++++++++++ .../src/components/primitives/Button.tsx | 50 +++---------- .../src/components/primitives/Card.tsx | 23 +----- .../src/components/primitives/Link.tsx | 34 +++++++-- .../components/primitives/StyleProvider.tsx | 32 ++++++++ .../src/components/primitives/StyledLink.tsx | 29 ++------ .../src/components/primitives/styles.ts | 74 +++++++++++++++++++ 12 files changed, 246 insertions(+), 173 deletions(-) create mode 100644 packages/gitbook/src/components/DocumentView/Table/styles.ts create mode 100644 packages/gitbook/src/components/TableOfContents/styles.ts create mode 100644 packages/gitbook/src/components/primitives/StyleProvider.tsx create mode 100644 packages/gitbook/src/components/primitives/styles.ts diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx index 2b07504f3f..decd177a13 100644 --- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx +++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx @@ -7,7 +7,8 @@ import { tcls } from '@/lib/tailwind'; import { type CustomizationAnnouncement, SiteInsightsLinkPosition } from '@gitbook/api'; import { Icon, type IconName } from '@gitbook/icons'; import { CONTAINER_STYLE } from '../layout'; -import { Link, linkStyles } from '../primitives'; +import { Link } from '../primitives'; +import { LinkStyles } from '../primitives/styles'; import { ANNOUNCEMENT_CSS_CLASS, ANNOUNCEMENT_STORAGE_KEY } from './constants'; /** @@ -58,7 +59,7 @@ export function AnnouncementBanner(props: { <div> {announcement.message} {hasLink ? ( - <div className={tcls(linkStyles, style.link, 'ml-1 inline')}> + <div className={tcls(LinkStyles, style.link, 'ml-1 inline')}> {contentRef?.icon ? ( <span className="mr-1 ml-2 *:inline"> {contentRef?.icon} diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx index c32fb01e9d..49aa7a1bc5 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx @@ -7,10 +7,11 @@ import { import { LinkBox, LinkOverlay } from '@/components/primitives'; import { Image } from '@/components/utils'; import { resolveContentRef } from '@/lib/references'; -import { type ClassValue, tcls } from '@/lib/tailwind'; +import { tcls } from '@/lib/tailwind'; import { RecordColumnValue } from './RecordColumnValue'; import type { TableRecordKV, TableViewProps } from './Table'; +import { RecordCardStyles } from './styles'; import { getRecordValue } from './utils'; export async function RecordCard( @@ -143,35 +144,12 @@ export async function RecordCard( </div> ); - const style = [ - 'group', - 'grid', - 'shadow-1xs', - 'shadow-tint-9/1', - 'depth-flat:shadow-none', - 'rounded', - 'straight-corners:rounded-none', - 'circular-corners:rounded-xl', - 'dark:shadow-transparent', - - 'before:pointer-events-none', - 'before:grid-area-1-1', - 'before:transition-shadow', - 'before:w-full', - 'before:h-full', - 'before:rounded-[inherit]', - 'before:ring-1', - 'before:ring-tint-12/2', - 'before:z-10', - 'before:relative', - ] as ClassValue; - if (target && targetRef) { return ( // We don't use `Link` directly here because we could end up in a situation where // a link is rendered inside a link, which is not allowed in HTML. // It causes an hydration error in React. - <LinkBox href={target.href} className={tcls(style, 'hover:before:ring-tint-12/5')}> + <LinkBox href={target.href} classNames={['RecordCardStyles']}> <LinkOverlay href={target.href} insights={{ @@ -187,5 +165,5 @@ export async function RecordCard( ); } - return <div className={tcls(style)}>{body}</div>; + return <div className={tcls(RecordCardStyles)}>{body}</div>; } diff --git a/packages/gitbook/src/components/DocumentView/Table/styles.ts b/packages/gitbook/src/components/DocumentView/Table/styles.ts new file mode 100644 index 0000000000..3e33b06837 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Table/styles.ts @@ -0,0 +1,26 @@ +import type { ClassValue } from '@/lib/tailwind'; + +export const RecordCardStyles = [ + 'group', + 'grid', + 'shadow-1xs', + 'shadow-tint-9/1', + 'depth-flat:shadow-none', + 'rounded', + 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', + 'dark:shadow-transparent', + + 'before:pointer-events-none', + 'before:grid-area-1-1', + 'before:transition-shadow', + 'before:w-full', + 'before:h-full', + 'before:rounded-[inherit]', + 'before:ring-1', + 'before:ring-tint-12/2', + 'before:z-10', + 'before:relative', + + 'hover:before:ring-tint-12/5', +] as ClassValue; diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index 35f350a0c3..c00d1c6216 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -17,25 +17,7 @@ export async function PageLinkItem(props: { page: RevisionPageLink; context: Git <li className={tcls('flex', 'flex-col')}> <Link href={resolved?.href ?? '#'} - className={tcls( - 'flex', - 'justify-start', - 'items-center', - 'gap-3', - 'p-1.5', - 'pl-3', - 'text-sm', - 'transition-colors', - 'duration-100', - 'text-tint-strong/7', - 'rounded-md', - 'straight-corners:rounded-none', - 'circular-corners:rounded-xl', - 'before:content-none', - 'font-normal', - 'hover:bg-tint', - 'hover:text-tint-strong' - )} + classNames={['PageLinkItemStyles']} insights={{ type: 'link_click', link: { diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index b118501f4c..681df69080 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -66,42 +66,10 @@ function LinkItem( href={href} insights={insights} aria-current={isActive ? 'page' : undefined} - className={tcls( - 'group/toclink toclink relative transition-colors', - 'flex flex-row justify-between', - 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', - 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', - 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12', - 'before:contents[] before:-left-px before:absolute before:inset-y-0', - 'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px', - - isActive && [ - 'font-semibold', - 'sidebar-list-line:before:w-0.5', - - 'before:bg-primary-solid', - 'text-primary-subtle', - 'contrast-more:text-primary', - - 'sidebar-list-pill:bg-primary', - '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover', - '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover', - '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary', - '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary', - - 'hover:bg-primary-hover', - 'hover:text-primary', - 'hover:before:bg-primary-solid-hover', - 'sidebar-list-pill:hover:bg-primary-hover', - - 'contrast-more:text-primary', - 'contrast-more:hover:text-primary-strong', - 'contrast-more:bg-primary', - 'contrast-more:ring-1', - 'contrast-more:ring-primary', - 'contrast-more:hover:ring-primary-hover', - ] - )} + classNames={[ + 'ToggleableLinkItemStyles', + ...(isActive ? ['ToggleableLinkItemActiveStyles' as const] : []), + ]} > {children} </Link> diff --git a/packages/gitbook/src/components/TableOfContents/styles.ts b/packages/gitbook/src/components/TableOfContents/styles.ts new file mode 100644 index 0000000000..3ceb636941 --- /dev/null +++ b/packages/gitbook/src/components/TableOfContents/styles.ts @@ -0,0 +1,56 @@ +export const PageLinkItemStyles = [ + 'flex', + 'justify-start', + 'items-center', + 'gap-3', + 'p-1.5', + 'pl-3', + 'text-sm', + 'transition-colors', + 'duration-100', + 'text-tint-strong/7', + 'rounded-md', + 'straight-corners:rounded-none', + 'circular-corners:rounded-xl', + 'before:content-none', + 'font-normal', + 'hover:bg-tint', + 'hover:text-tint-strong', +]; + +export const ToggleableLinkItemStyles = [ + 'group/toclink toclink relative transition-colors', + 'flex flex-row justify-between', + 'circular-corners:rounded-2xl rounded-md straight-corners:rounded-none p-1.5 pl-3', + 'text-balance font-normal text-sm text-tint-strong/7 hover:bg-tint-hover hover:text-tint-strong contrast-more:text-tint-strong', + 'hover:contrast-more:text-tint-strong hover:contrast-more:ring-1 hover:contrast-more:ring-tint-12', + 'before:contents[] before:-left-px before:absolute before:inset-y-0', + 'sidebar-list-line:rounded-l-none sidebar-list-line:before:w-px sidebar-list-default:[&+div_a]:rounded-l-none [&+div_a]:pl-5 sidebar-list-default:[&+div_a]:before:w-px', +]; + +export const ToggleableLinkItemActiveStyles = [ + 'font-semibold', + 'sidebar-list-line:before:w-0.5', + + 'before:bg-primary-solid', + 'text-primary-subtle', + 'contrast-more:text-primary', + + 'sidebar-list-pill:bg-primary', + '[html.sidebar-list-pill.theme-muted_&]:bg-primary-hover', + '[html.sidebar-list-pill.theme-bold.tint_&]:bg-primary-hover', + '[html.sidebar-filled.sidebar-list-pill.theme-muted_&]:bg-primary', + '[html.sidebar-filled.sidebar-list-pill.theme-bold.tint_&]:bg-primary', + + 'hover:bg-primary-hover', + 'hover:text-primary', + 'hover:before:bg-primary-solid-hover', + 'sidebar-list-pill:hover:bg-primary-hover', + + 'contrast-more:text-primary', + 'contrast-more:hover:text-primary-strong', + 'contrast-more:bg-primary', + 'contrast-more:ring-1', + 'contrast-more:ring-primary', + 'contrast-more:hover:ring-primary-hover', +]; diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx index 58697d5b7f..4ccc10b6b7 100644 --- a/packages/gitbook/src/components/primitives/Button.tsx +++ b/packages/gitbook/src/components/primitives/Button.tsx @@ -6,6 +6,7 @@ import { type ClassValue, tcls } from '@/lib/tailwind'; import { Icon, type IconName } from '@gitbook/icons'; import { Link, type LinkInsightsProps } from './Link'; +import { useClassnames } from './StyleProvider'; type ButtonProps = { href?: string; @@ -18,7 +19,7 @@ type ButtonProps = { } & LinkInsightsProps & HTMLAttributes<HTMLElement>; -const variantClasses = { +export const variantClasses = { primary: [ 'bg-primary-solid', 'text-contrast-primary-solid', @@ -69,49 +70,15 @@ export function Button({ const sizeClasses = sizes[size] || sizes.default; - const domClassName = tcls( - 'button', - 'inline-flex', - 'items-center', - 'gap-2', - 'rounded-md', - 'straight-corners:rounded-none', - 'circular-corners:rounded-full', - // 'place-self-start', - - 'ring-1', - 'ring-tint', - 'hover:ring-tint-hover', - - 'shadow-sm', - 'shadow-tint', - 'dark:shadow-tint-1', - 'hover:shadow-md', - 'active:shadow-none', - 'depth-flat:shadow-none', - - 'contrast-more:ring-tint-12', - 'contrast-more:hover:ring-2', - 'contrast-more:hover:ring-tint-12', - - 'hover:scale-104', - 'depth-flat:hover:scale-100', - 'active:scale-100', - 'transition-all', - - 'grow-0', - 'shrink-0', - 'truncate', - variantClasses[variant], - sizeClasses, - className - ); + const domClassName = tcls(variantClasses[variant], sizeClasses, className); + const buttonOnlyClassNames = useClassnames(['ButtonStyles']); if (href) { return ( <Link href={href} className={domClassName} + classNames={['ButtonStyles']} insights={insights} aria-label={label} target={target} @@ -124,7 +91,12 @@ export function Button({ } return ( - <button type="button" className={domClassName} aria-label={label} {...rest}> + <button + type="button" + className={tcls(domClassName, buttonOnlyClassNames)} + aria-label={label} + {...rest} + > {icon ? <Icon icon={icon} className={tcls('size-[1em]')} /> : null} {iconOnly ? null : label} </button> diff --git a/packages/gitbook/src/components/primitives/Card.tsx b/packages/gitbook/src/components/primitives/Card.tsx index a796ba9eff..1195c78476 100644 --- a/packages/gitbook/src/components/primitives/Card.tsx +++ b/packages/gitbook/src/components/primitives/Card.tsx @@ -17,28 +17,7 @@ export async function Card( const { title, leadingIcon, href, preTitle, postTitle, style, insights } = props; return ( - <Link - href={href} - className={tcls( - 'group', - 'flex', - 'flex-row', - 'justify-between', - 'items-center', - 'gap-4', - 'ring-1', - 'ring-tint-subtle', - 'rounded', - 'straight-corners:rounded-none', - 'circular-corners:rounded-2xl', - 'px-5', - 'py-3', - 'transition-shadow', - 'hover:ring-primary-hover', - style - )} - insights={insights} - > + <Link href={href} className={tcls(style)} classNames={['CardStyles']} insights={insights}> {leadingIcon} <span className={tcls('flex', 'flex-col', 'flex-1')}> {preTitle ? ( diff --git a/packages/gitbook/src/components/primitives/Link.tsx b/packages/gitbook/src/components/primitives/Link.tsx index ff9f5ebde1..e15a01ca11 100644 --- a/packages/gitbook/src/components/primitives/Link.tsx +++ b/packages/gitbook/src/components/primitives/Link.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { tcls } from '@/lib/tailwind'; import { type TrackEventInput, useTrackEvent } from '../Insights'; +import { type DesignTokenName, useClassnames } from './StyleProvider'; // Props from Next, which includes NextLinkProps and all the things anchor elements support. type BaseLinkProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps> & @@ -25,6 +26,8 @@ export type LinkProps = Omit<BaseLinkProps, 'href'> & LinkInsightsProps & { /** Enforce href is passed as a string (not a URL). */ href: string; + /** This is a temporary solution designed to reduce the number of tailwind class passed to the client */ + classNames?: DesignTokenName[]; }; /** @@ -35,8 +38,9 @@ export const Link = React.forwardRef(function Link( props: LinkProps, ref: React.Ref<HTMLAnchorElement> ) { - const { href, prefetch, children, insights, ...domProps } = props; + const { href, prefetch, children, insights, classNames, className, ...domProps } = props; const trackEvent = useTrackEvent(); + const forwardedClassNames = useClassnames(classNames || []); // Use a real anchor tag for external links,s and a Next.js Link for internal links. // If we use a NextLink for external links, Nextjs won't rerender the top-level layouts. @@ -63,14 +67,27 @@ export const Link = React.forwardRef(function Link( // as this will be rendered on the server and it could result in a mismatch. if (isExternalLink(href)) { return ( - <a ref={ref} {...domProps} href={href} onClick={onClick}> + <a + ref={ref} + className={tcls(...forwardedClassNames, className)} + {...domProps} + href={href} + onClick={onClick} + > {children} </a> ); } return ( - <NextLink ref={ref} href={href} prefetch={prefetch} {...domProps} onClick={onClick}> + <NextLink + ref={ref} + href={href} + prefetch={prefetch} + className={tcls(...forwardedClassNames, className)} + {...domProps} + onClick={onClick} + > {children} </NextLink> ); @@ -81,12 +98,17 @@ export const Link = React.forwardRef(function Link( * It is used to create a clickable area that can contain other elements. */ export const LinkBox = React.forwardRef(function LinkBox( - props: React.BaseHTMLAttributes<HTMLDivElement>, + props: React.BaseHTMLAttributes<HTMLDivElement> & { classNames?: DesignTokenName[] }, ref: React.Ref<HTMLDivElement> ) { - const { children, className, ...domProps } = props; + const { children, className, classNames, ...domProps } = props; + const forwardedClassNames = useClassnames(classNames || []); return ( - <div ref={ref} {...domProps} className={tcls('elevate-link relative', className)}> + <div + ref={ref} + {...domProps} + className={tcls('elevate-link relative', className, forwardedClassNames)} + > {children} </div> ); diff --git a/packages/gitbook/src/components/primitives/StyleProvider.tsx b/packages/gitbook/src/components/primitives/StyleProvider.tsx new file mode 100644 index 0000000000..a1e1b1fe03 --- /dev/null +++ b/packages/gitbook/src/components/primitives/StyleProvider.tsx @@ -0,0 +1,32 @@ +'use client'; +import type { ClassValue } from '@/lib/tailwind'; + +import { RecordCardStyles } from '../DocumentView/Table/styles'; +import { + PageLinkItemStyles, + ToggleableLinkItemActiveStyles, + ToggleableLinkItemStyles, +} from '../TableOfContents/styles'; +import { ButtonStyles, CardStyles, LinkStyles } from './styles'; + +const styles = { + LinkStyles, + CardStyles, + ButtonStyles, + RecordCardStyles, + PageLinkItemStyles, + ToggleableLinkItemStyles, + ToggleableLinkItemActiveStyles, +}; + +export type DesignTokenName = keyof typeof styles; + +/** + * Get the class names for the given design token names. + * TODO: remove this once we figure out a better solution. Likely with TW4. + * @param names The design token names to get class names for. + * @returns The class names for the given design token names. + */ +export function useClassnames(names: DesignTokenName[]): ClassValue[] { + return names.flatMap((name) => styles[name] || []); +} diff --git a/packages/gitbook/src/components/primitives/StyledLink.tsx b/packages/gitbook/src/components/primitives/StyledLink.tsx index 3fb346f5f6..f3b391b67e 100644 --- a/packages/gitbook/src/components/primitives/StyledLink.tsx +++ b/packages/gitbook/src/components/primitives/StyledLink.tsx @@ -1,35 +1,18 @@ -import { type ClassValue, tcls } from '@/lib/tailwind'; +import type { ClassValue } from '@/lib/tailwind'; import { Link, type LinkProps } from '../primitives/Link'; - -export const linkStyles = [ - 'underline', - 'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin. - 'underline-offset-2', - 'links-accent:underline-offset-4', - - 'links-default:decoration-primary/6', - 'links-default:text-primary-subtle', - 'links-default:hover:text-primary-strong', - 'links-default:contrast-more:text-primary', - 'links-default:contrast-more:hover:text-primary-strong', - - 'links-accent:decoration-primary-subtle', - 'links-accent:hover:decoration-[3px]', - 'links-accent:hover:[text-decoration-skip-ink:none]', - - 'transition-all', - 'duration-100', -]; +import type { DesignTokenName } from './StyleProvider'; /** * Styled version of Link component. */ export function StyledLink(props: Omit<LinkProps, 'style'> & { className?: ClassValue }) { - const { className, ...rest } = props; + const { classNames, ...rest } = props; + + const classNamesToForward: DesignTokenName[] = [...(classNames || []), 'LinkStyles']; return ( - <Link {...rest} className={tcls(linkStyles, className)}> + <Link {...rest} classNames={classNamesToForward}> {props.children} </Link> ); diff --git a/packages/gitbook/src/components/primitives/styles.ts b/packages/gitbook/src/components/primitives/styles.ts new file mode 100644 index 0000000000..9d465aac4a --- /dev/null +++ b/packages/gitbook/src/components/primitives/styles.ts @@ -0,0 +1,74 @@ +import type { ClassValue } from '@/lib/tailwind'; + +export const ButtonStyles = [ + 'button', + 'inline-flex', + 'items-center', + 'gap-2', + 'rounded-md', + 'straight-corners:rounded-none', + 'circular-corners:rounded-full', + // 'place-self-start', + + 'ring-1', + 'ring-tint', + 'hover:ring-tint-hover', + + 'shadow-sm', + 'shadow-tint', + 'dark:shadow-tint-1', + 'hover:shadow-md', + 'active:shadow-none', + 'depth-flat:shadow-none', + + 'contrast-more:ring-tint-12', + 'contrast-more:hover:ring-2', + 'contrast-more:hover:ring-tint-12', + + 'hover:scale-104', + 'depth-flat:hover:scale-100', + 'active:scale-100', + 'transition-all', + + 'grow-0', + 'shrink-0', + 'truncate', +] as ClassValue[]; + +export const CardStyles = [ + 'group', + 'flex', + 'flex-row', + 'justify-between', + 'items-center', + 'gap-4', + 'ring-1', + 'ring-tint-subtle', + 'rounded', + 'straight-corners:rounded-none', + 'circular-corners:rounded-2xl', + 'px-5', + 'py-3', + 'transition-shadow', + 'hover:ring-primary-hover', +] as ClassValue[]; + +export const LinkStyles = [ + 'underline', + 'decoration-[max(0.07em,1px)]', // Set the underline to be proportional to the font size, with a minimum. The default is too thin. + 'underline-offset-2', + 'links-accent:underline-offset-4', + + 'links-default:decoration-primary/6', + 'links-default:text-primary-subtle', + 'links-default:hover:text-primary-strong', + 'links-default:contrast-more:text-primary', + 'links-default:contrast-more:hover:text-primary-strong', + + 'links-accent:decoration-primary-subtle', + 'links-accent:hover:decoration-[3px]', + 'links-accent:hover:[text-decoration-skip-ink:none]', + + 'transition-all', + 'duration-100', +] as ClassValue[];