mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Migrate to Next 16 (#4162)
This commit is contained in:
@@ -19,6 +19,9 @@ inputs:
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
required: true
|
||||
headSha:
|
||||
description: 'Git ref to deploy, used for the deploymentId'
|
||||
required: false
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
@@ -59,6 +62,16 @@ runs:
|
||||
GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE
|
||||
GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX
|
||||
GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL
|
||||
- name: Inject build env vars
|
||||
if: ${{ inputs.environment == 'preview' }}
|
||||
shell: bash
|
||||
run: |
|
||||
HEAD_SHA=$(git rev-parse HEAD)
|
||||
echo "resolved HEAD_SHA: $HEAD_SHA"
|
||||
echo "GITBOOK_HEAD_SHA=$HEAD_SHA" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "GITBOOK_RUNTIME=vercel" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
cat .vercel/.env.${{ inputs.environment }}.local
|
||||
- name: Build Project Artifacts
|
||||
run: bun run vercel build --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
|
||||
shell: bash
|
||||
@@ -66,6 +79,7 @@ runs:
|
||||
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
|
||||
GITBOOK_RUNTIME: vercel
|
||||
GITBOOK_HEAD_SHA: ${{ inputs.headSha }}
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
id: deploy
|
||||
shell: bash
|
||||
|
||||
@@ -40,6 +40,7 @@ jobs:
|
||||
vercelToken: ${{ secrets.VERCEL_TOKEN }}
|
||||
opItem: op://gitbook-open/2v-preview
|
||||
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
|
||||
headSha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
deploy-v2-cloudflare:
|
||||
name: Deploy v2 to Cloudflare Worker (preview)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+5
-3
@@ -17,8 +17,9 @@
|
||||
"@types/react-dom": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"esbuild": "0.24.2",
|
||||
"axios": "1.8.4"
|
||||
"esbuild": "0.27.3",
|
||||
"axios": "1.8.4",
|
||||
"@radix-ui/react-slot": "1.2.4"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -59,6 +60,7 @@
|
||||
},
|
||||
"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"
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"next@16.2.3": "patches/next@16.2.3.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// @ts-check
|
||||
|
||||
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
|
||||
const deploymentId =
|
||||
process.env.GITBOOK_RUNTIME === 'cloudflare'
|
||||
? undefined
|
||||
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
|
||||
/**
|
||||
* @type {import('next').NextConfig}
|
||||
*/
|
||||
const nextConfig = {
|
||||
deploymentId: process.env.GITHUB_SHA || Date.now().toString(), // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
deploymentId: deploymentId?.slice(0, 32), // Vercel's deployment ID has a max length of 32 characters
|
||||
experimental: {
|
||||
// This is needed to throw "forbidden" when the api token expired during revalidation
|
||||
authInterrupts: true,
|
||||
@@ -18,10 +24,12 @@ const nextConfig = {
|
||||
|
||||
// Since content is fully static, we don't want to fetch on hover again
|
||||
optimisticClientCache: false,
|
||||
// Disable splitting the RSC in like 5 chunks
|
||||
prefetchInlining: true,
|
||||
},
|
||||
|
||||
env: {
|
||||
BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
BUILD_VERSION: (process.env.GITBOOK_HEAD_SHA ?? process.env.GITHUB_SHA ?? '').slice(0, 7),
|
||||
|
||||
// GitBook envs
|
||||
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"main": "default.js",
|
||||
"name": "gitbook-open-v2-server",
|
||||
"keep_names": false,
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_date": "2026-04-02",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"main": "middleware.js",
|
||||
"name": "gitbook-open-v2",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_date": "2026-04-02",
|
||||
"keep_names": false,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"@gitbook/react-openapi": "workspace:*",
|
||||
"@mermaid-js/mermaid-zenuml": "^0.2.2",
|
||||
"@modelcontextprotocol/sdk": "1.17.5",
|
||||
"@opennextjs/aws": "^3.8.5",
|
||||
"@opennextjs/cloudflare": "^1.14.4",
|
||||
"@opennextjs/aws": "3.10.1",
|
||||
"@opennextjs/cloudflare": "1.19.1",
|
||||
"@panzoom/panzoom": "^4.6.1",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.12",
|
||||
@@ -43,6 +43,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsontoxml": "^1.0.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leven": "^4.1.0",
|
||||
"mcp-handler": "^1.0.2",
|
||||
"mdast-util-from-markdown": "^2.0.2",
|
||||
"mdast-util-frontmatter": "^2.0.1",
|
||||
@@ -53,8 +54,7 @@
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"motion": "^12.23.24",
|
||||
"leven": "^4.1.0",
|
||||
"next": "15.4.11",
|
||||
"next": "^16.2.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -62,8 +62,8 @@
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.3",
|
||||
"quick-lru": "^7.0.1",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
@@ -98,8 +98,8 @@
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/rison": "^0.0.9",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"bun-types": "catalog:",
|
||||
@@ -112,17 +112,17 @@
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.43.0",
|
||||
"wrangler": "^4.79.0",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "./scripts/generate.sh",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math",
|
||||
"dev": "env-cmd --silent -f ../../.env.local next",
|
||||
"build": "next build",
|
||||
"build:local": "GITBOOK_URL=http://localhost:3000 next build",
|
||||
"dev": "env-cmd --silent -f ../../.env.local next --webpack",
|
||||
"build": "next build --webpack",
|
||||
"build:local": "GITBOOK_URL=http://localhost:3000 next build --webpack",
|
||||
"start": "GITBOOK_URL=http://localhost:3000 next start",
|
||||
"build:cloudflare": "opennextjs-cloudflare build",
|
||||
"build:cloudflare": "GITBOOK_RUNTIME=cloudflare opennextjs-cloudflare build",
|
||||
"dev:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
@@ -138,5 +138,9 @@
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
body.tags.forEach((tag) => {
|
||||
logger.log(`Revalidating tag: ${tag}`);
|
||||
revalidateTag(tag);
|
||||
revalidateTag(tag, { expire: 0 }); // Force revalidation without waiting for the next scheduled revalidation
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import {
|
||||
type ContentRef,
|
||||
type CustomizationContentLink,
|
||||
type CustomizationHeaderItem,
|
||||
type CustomizationHeaderPreset,
|
||||
SiteInsightsLinkPosition,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import type React from 'react';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import {
|
||||
SiteAuthLoginButton,
|
||||
SiteAuthLoginDropdownMenuItem,
|
||||
SiteAuthLoginLink,
|
||||
} from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { Button, Link, ToggleChevron } from '../primitives';
|
||||
import {
|
||||
type DropdownButtonProps,
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
} from '../primitives/DropdownMenu';
|
||||
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { DropdownMenuItem } from '../primitives/DropdownMenu';
|
||||
import { HeaderLinkDropdown, HeaderLinkNavItem } from './HeaderLinkDropdown';
|
||||
|
||||
export async function HeaderLink(props: {
|
||||
context: GitBookSiteContext;
|
||||
@@ -39,29 +26,22 @@ export async function HeaderLink(props: {
|
||||
|
||||
if (link.links && link.links.length > 0) {
|
||||
return (
|
||||
<DropdownMenu
|
||||
className={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
|
||||
button={
|
||||
!target || !link.to ? (
|
||||
<HeaderItemDropdown headerPreset={headerPreset} title={title} />
|
||||
) : (
|
||||
<HeaderLinkNavItem
|
||||
linkTarget={link.to}
|
||||
linkStyle={linkStyle}
|
||||
headerPreset={headerPreset}
|
||||
title={title}
|
||||
isDropdown
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={isSiteAuthLoginHref(context.linker, target.href)}
|
||||
/>
|
||||
)
|
||||
<HeaderLinkDropdown
|
||||
headerPreset={headerPreset}
|
||||
title={title}
|
||||
hasTarget={!!target}
|
||||
linkTarget={link.to}
|
||||
linkStyle={linkStyle}
|
||||
href={target?.href}
|
||||
isSiteAuthLoginHref={
|
||||
target ? isSiteAuthLoginHref(context.linker, target.href) : false
|
||||
}
|
||||
openOnHover={true}
|
||||
dropdownClassName={`shrink ${customization.styling.search === 'prominent' ? 'right-0 left-auto' : null}`}
|
||||
>
|
||||
{link.links.map((subLink, index) => (
|
||||
<SubHeaderLink key={index} {...props} link={subLink} />
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</HeaderLinkDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,148 +62,6 @@ export async function HeaderLink(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export type HeaderLinkNavItemProps = {
|
||||
linkTarget: ContentRef;
|
||||
linkStyle: NonNullable<CustomizationHeaderItem['style']>;
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
href?: string;
|
||||
isDropdown: boolean;
|
||||
isSiteAuthLoginHref: boolean;
|
||||
} & DropdownButtonProps<HTMLElement>;
|
||||
|
||||
function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
|
||||
const { linkStyle, ...rest } = props;
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
case 'button-primary':
|
||||
return <HeaderItemButton {...rest} linkStyle={linkStyle} />;
|
||||
case 'link':
|
||||
return <HeaderItemLink {...rest} />;
|
||||
default:
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
}
|
||||
|
||||
function HeaderItemButton(
|
||||
props: Omit<HeaderLinkNavItemProps, 'linkStyle'> & {
|
||||
linkStyle: 'button-secondary' | 'button-primary';
|
||||
}
|
||||
) {
|
||||
const {
|
||||
linkTarget,
|
||||
linkStyle,
|
||||
headerPreset,
|
||||
title,
|
||||
href,
|
||||
isDropdown,
|
||||
isSiteAuthLoginHref,
|
||||
...rest
|
||||
} = props;
|
||||
const variant = (() => {
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
return 'header';
|
||||
case 'button-primary':
|
||||
return 'primary';
|
||||
default:
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
})();
|
||||
const sharedProps: React.ComponentProps<typeof Button> = {
|
||||
href,
|
||||
variant,
|
||||
size: 'medium' as const,
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
label: title,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginButton {...sharedProps} />
|
||||
) : (
|
||||
<Button {...sharedProps} />
|
||||
);
|
||||
}
|
||||
|
||||
function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPreset }) {
|
||||
return tcls(
|
||||
'flex items-center gap-1',
|
||||
'shrink',
|
||||
'contrast-more:underline',
|
||||
'truncate',
|
||||
|
||||
'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]',
|
||||
'links-accent:py-0.5', // Prevent underline from being cut off at the bottom
|
||||
|
||||
'theme-bold:text-header-link',
|
||||
'hover:theme-bold:text-header-link/7!'
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
||||
const { linkTarget, headerPreset, title, isDropdown, href, isSiteAuthLoginHref, ...rest } =
|
||||
props;
|
||||
const sharedProps = {
|
||||
href: href ?? '#',
|
||||
className: getHeaderLinkClassName({ headerPreset }),
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginLink {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</SiteAuthLoginLink>
|
||||
) : (
|
||||
<Link {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderItemDropdown(
|
||||
props: {
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
} & DropdownButtonProps<HTMLElement>
|
||||
) {
|
||||
const { headerPreset, title, ...rest } = props;
|
||||
return (
|
||||
<span
|
||||
className={tcls(getHeaderLinkClassName({ headerPreset }), 'cursor-default')}
|
||||
{...rest}
|
||||
>
|
||||
{title}
|
||||
<ToggleChevron />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
async function SubHeaderLink(props: {
|
||||
context: GitBookSiteContext;
|
||||
link: CustomizationContentLink;
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
type ContentRef,
|
||||
type CustomizationHeaderPreset,
|
||||
SiteInsightsLinkPosition,
|
||||
} from '@gitbook/api';
|
||||
import assertNever from 'assert-never';
|
||||
import type React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { SiteAuthLoginButton, SiteAuthLoginLink } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { Button, Link, ToggleChevron } from '../primitives';
|
||||
import { type DropdownButtonProps, DropdownMenu } from '../primitives/DropdownMenu';
|
||||
|
||||
/**
|
||||
* Client component wrapping the DropdownMenu for header links.
|
||||
* Accepts serializable props to avoid Flight lazy chunk wrappers reaching Radix's Slot.
|
||||
*/
|
||||
export function HeaderLinkDropdown(props: {
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
hasTarget: boolean;
|
||||
linkTarget: ContentRef | null;
|
||||
linkStyle: 'link' | 'button-secondary' | 'button-primary';
|
||||
href?: string;
|
||||
isSiteAuthLoginHref: boolean;
|
||||
dropdownClassName: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const {
|
||||
headerPreset,
|
||||
title,
|
||||
hasTarget,
|
||||
linkTarget,
|
||||
linkStyle,
|
||||
href,
|
||||
isSiteAuthLoginHref,
|
||||
dropdownClassName,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
className={dropdownClassName}
|
||||
button={
|
||||
!hasTarget || !linkTarget ? (
|
||||
<HeaderItemDropdown headerPreset={headerPreset} title={title} />
|
||||
) : (
|
||||
<HeaderLinkNavItem
|
||||
linkTarget={linkTarget}
|
||||
linkStyle={linkStyle}
|
||||
headerPreset={headerPreset}
|
||||
title={title}
|
||||
isDropdown
|
||||
href={href}
|
||||
isSiteAuthLoginHref={isSiteAuthLoginHref}
|
||||
/>
|
||||
)
|
||||
}
|
||||
openOnHover={true}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export type HeaderLinkNavItemProps = {
|
||||
linkTarget: ContentRef;
|
||||
linkStyle: NonNullable<'link' | 'button-secondary' | 'button-primary'>;
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
href?: string;
|
||||
isDropdown: boolean;
|
||||
isSiteAuthLoginHref: boolean;
|
||||
} & DropdownButtonProps<HTMLElement>;
|
||||
|
||||
export function HeaderLinkNavItem(props: HeaderLinkNavItemProps) {
|
||||
const { linkStyle, ...rest } = props;
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
case 'button-primary':
|
||||
return <HeaderItemButton {...rest} linkStyle={linkStyle} />;
|
||||
case 'link':
|
||||
return <HeaderItemLink {...rest} />;
|
||||
default:
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
}
|
||||
|
||||
function HeaderItemButton(
|
||||
props: Omit<HeaderLinkNavItemProps, 'linkStyle'> & {
|
||||
linkStyle: 'button-secondary' | 'button-primary';
|
||||
}
|
||||
) {
|
||||
const {
|
||||
linkTarget,
|
||||
linkStyle,
|
||||
headerPreset,
|
||||
title,
|
||||
href,
|
||||
isDropdown,
|
||||
isSiteAuthLoginHref,
|
||||
...rest
|
||||
} = props;
|
||||
const variant = (() => {
|
||||
switch (linkStyle) {
|
||||
case 'button-secondary':
|
||||
return 'header';
|
||||
case 'button-primary':
|
||||
return 'primary';
|
||||
default:
|
||||
assertNever(linkStyle);
|
||||
}
|
||||
})();
|
||||
const sharedProps: React.ComponentProps<typeof Button> = {
|
||||
href,
|
||||
variant,
|
||||
size: 'medium' as const,
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
label: title,
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginButton {...sharedProps} />
|
||||
) : (
|
||||
<Button {...sharedProps} />
|
||||
);
|
||||
}
|
||||
|
||||
function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPreset }) {
|
||||
return tcls(
|
||||
'flex items-center gap-1',
|
||||
'shrink',
|
||||
'contrast-more:underline',
|
||||
'truncate',
|
||||
|
||||
'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]',
|
||||
'links-accent:py-0.5', // Prevent underline from being cut off at the bottom
|
||||
|
||||
'theme-bold:text-header-link',
|
||||
'hover:theme-bold:text-header-link/7!'
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderItemLink(props: Omit<HeaderLinkNavItemProps, 'linkStyle'>) {
|
||||
const {
|
||||
linkTarget,
|
||||
headerPreset,
|
||||
title,
|
||||
isDropdown,
|
||||
href,
|
||||
isSiteAuthLoginHref,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
const sharedProps = {
|
||||
href: href ?? '#',
|
||||
className: tcls(getHeaderLinkClassName({ headerPreset }), className),
|
||||
insights: {
|
||||
type: 'link_click' as const,
|
||||
link: {
|
||||
target: linkTarget,
|
||||
position: SiteInsightsLinkPosition.Header,
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
|
||||
return isSiteAuthLoginHref ? (
|
||||
<SiteAuthLoginLink {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</SiteAuthLoginLink>
|
||||
) : (
|
||||
<Link {...sharedProps}>
|
||||
{title}
|
||||
{isDropdown ? <ToggleChevron /> : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderItemDropdown(
|
||||
props: {
|
||||
headerPreset: CustomizationHeaderPreset;
|
||||
title: string;
|
||||
} & DropdownButtonProps<HTMLElement>
|
||||
) {
|
||||
const { headerPreset, title, className, ...rest } = props;
|
||||
return (
|
||||
<span
|
||||
className={tcls(getHeaderLinkClassName({ headerPreset }), 'cursor-default', className)}
|
||||
{...rest}
|
||||
>
|
||||
{title}
|
||||
<ToggleChevron />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
SiteInsightsLinkPosition,
|
||||
type SiteSocialAccount,
|
||||
} from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type React from 'react';
|
||||
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
@@ -15,13 +14,12 @@ import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { SocialAccountLink } from '../Footer/SocialAccounts';
|
||||
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { ToggleChevron } from '../primitives';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownSubMenu,
|
||||
} from '../primitives/DropdownMenu';
|
||||
import { HeaderLinkMoreDropdown } from './HeaderLinkMoreClient';
|
||||
import styles from './headerLinks.module.css';
|
||||
|
||||
/**
|
||||
@@ -35,32 +33,11 @@ export function HeaderLinkMore(props: {
|
||||
}) {
|
||||
const { label, links, context, socialAccounts } = props;
|
||||
|
||||
const renderButton = (
|
||||
<button
|
||||
type="button"
|
||||
className={tcls(
|
||||
'text-tint',
|
||||
'hover:text-primary',
|
||||
'dark:hover:text-primary',
|
||||
'theme-bold:text-header-link',
|
||||
'theme-bold:hover:text-header-link/8',
|
||||
'flex',
|
||||
'gap-1',
|
||||
'items-center'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<Icon icon="ellipsis" className={tcls('size-4')} />
|
||||
<ToggleChevron />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`${styles.linkEllipsis} z-20 items-center`}>
|
||||
<DropdownMenu
|
||||
button={renderButton}
|
||||
openOnHover={true}
|
||||
className={tcls(
|
||||
<HeaderLinkMoreDropdown
|
||||
label={label}
|
||||
dropdownClassName={tcls(
|
||||
'max-md:right-0 max-md:left-auto',
|
||||
context.customization.styling.search === 'prominent' && 'right-0 left-auto'
|
||||
)}
|
||||
@@ -75,7 +52,7 @@ export function HeaderLinkMore(props: {
|
||||
account={account}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</HeaderLinkMoreDropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import type React from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { ToggleChevron } from '../primitives';
|
||||
import { DropdownMenu } from '../primitives/DropdownMenu';
|
||||
|
||||
/**
|
||||
* Client component for the "More" header link dropdown.
|
||||
* Creates the trigger button internally to avoid Flight lazy chunk wrappers reaching Radix's Slot.
|
||||
*/
|
||||
export function HeaderLinkMoreDropdown(props: {
|
||||
label: React.ReactNode;
|
||||
dropdownClassName: ClassValue;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { label, dropdownClassName, children } = props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
className={tcls(
|
||||
'text-tint',
|
||||
'hover:text-primary',
|
||||
'dark:hover:text-primary',
|
||||
'theme-bold:text-header-link',
|
||||
'theme-bold:hover:text-header-link/8',
|
||||
'flex',
|
||||
'gap-1',
|
||||
'items-center'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<Icon icon="ellipsis" className={tcls('size-4')} />
|
||||
<ToggleChevron />
|
||||
</button>
|
||||
}
|
||||
openOnHover={true}
|
||||
className={dropdownClassName}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { SiteSpace } from '@gitbook/api';
|
||||
import { useMemo } from 'react';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
|
||||
import { DropdownMenu } from '../primitives/DropdownMenu';
|
||||
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
||||
import type { ButtonProps } from '../primitives';
|
||||
import { SpacesDropdownClient } from './SpacesDropdownClient';
|
||||
|
||||
// Memoized regex for checking if a string starts with an emoji
|
||||
const EMOJI_REGEX = /^\p{Emoji}/u;
|
||||
@@ -21,43 +20,34 @@ export function SpacesDropdown(props: {
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
variant?: ButtonProps['variant'];
|
||||
icon?: ButtonProps['icon'];
|
||||
icon?: IconName;
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const dropdownClassName = 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
|
||||
);
|
||||
|
||||
const slimSpaces = siteSpaces.map((siteSp) => ({
|
||||
id: siteSp.id,
|
||||
title: getLocalizedTitle(siteSp, currentLanguage),
|
||||
url: getSiteSpaceURL(context, siteSp),
|
||||
isActive: siteSp.id === siteSpace.id,
|
||||
spaceId: siteSp.space.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<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={
|
||||
<Button
|
||||
icon={icon}
|
||||
data-testid="space-dropdown-button"
|
||||
size="small"
|
||||
variant={variant}
|
||||
trailing={<ToggleChevron />}
|
||||
className={tcls('bg-tint-base', className)}
|
||||
>
|
||||
<span className="button-content">
|
||||
{getLocalizedTitle(siteSpace, currentLanguage)}
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SpacesDropdownMenuItems
|
||||
slimSpaces={siteSpaces.map((siteSp) => ({
|
||||
id: siteSp.id,
|
||||
title: getLocalizedTitle(siteSp, currentLanguage),
|
||||
url: getSiteSpaceURL(context, siteSp),
|
||||
isActive: siteSp.id === siteSpace.id,
|
||||
spaceId: siteSp.space.id,
|
||||
}))}
|
||||
curPath={siteSpace.path}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
<SpacesDropdownClient
|
||||
title={getLocalizedTitle(siteSpace, currentLanguage)}
|
||||
icon={icon}
|
||||
variant={variant}
|
||||
className={className}
|
||||
dropdownClassName={dropdownClassName}
|
||||
slimSpaces={slimSpaces}
|
||||
curPath={siteSpace.path}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,9 +59,8 @@ export function TranslationsDropdown(props: {
|
||||
}) {
|
||||
const { context, siteSpace, siteSpaces, className } = props;
|
||||
|
||||
// Memoize the emoji check to avoid repeated regex execution
|
||||
const title = getLocalizedTitle(siteSpace, context.locale);
|
||||
const hasEmojiPrefix = useMemo(() => startsWithEmoji(title), [title]);
|
||||
const hasEmojiPrefix = startsWithEmoji(title);
|
||||
|
||||
return (
|
||||
<SpacesDropdown
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
|
||||
import { DropdownMenu } from '../primitives/DropdownMenu';
|
||||
import { SpacesDropdownMenuItems } from './SpacesDropdownMenuItem';
|
||||
|
||||
/**
|
||||
* Client component that creates the Button trigger and DropdownMenu internally,
|
||||
* avoiding server→client element serialization through Radix's Slot.
|
||||
*/
|
||||
export function SpacesDropdownClient(props: {
|
||||
title: string;
|
||||
icon?: IconName;
|
||||
variant: ButtonProps['variant'];
|
||||
className?: ClassValue;
|
||||
dropdownClassName: string;
|
||||
slimSpaces: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
isActive: boolean;
|
||||
spaceId: string;
|
||||
}>;
|
||||
curPath: string;
|
||||
}) {
|
||||
const { title, icon, variant, className, dropdownClassName, slimSpaces, curPath } = props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
className={dropdownClassName}
|
||||
button={
|
||||
<Button
|
||||
icon={icon}
|
||||
data-testid="space-dropdown-button"
|
||||
size="small"
|
||||
variant={variant}
|
||||
trailing={<ToggleChevron />}
|
||||
className={tcls('bg-tint-base', className)}
|
||||
>
|
||||
<span className="button-content">{title}</span>
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SpacesDropdownMenuItems slimSpaces={slimSpaces} curPath={curPath} />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -12,10 +12,12 @@ export function HoverCardRoot(props: RadixHoverCard.HoverCardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// We wrap the Trigger child in a span to avoid react slot seeing a lazy chunk
|
||||
export function HoverCardTrigger(props: RadixHoverCard.HoverCardTriggerProps) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<RadixHoverCard.Trigger asChild {...props}>
|
||||
{props.children}
|
||||
<RadixHoverCard.Trigger asChild {...rest}>
|
||||
<span>{children}</span>
|
||||
</RadixHoverCard.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@gitbook/api';
|
||||
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
|
||||
import { parse as parseCacheControl } from '@tusbar/cache-control';
|
||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
import { cacheLife, cacheTag } from 'next/cache';
|
||||
import { cache } from '../cache';
|
||||
import { DataFetcherError, wrapCacheDataFetcherError } from './errors';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
@@ -21,7 +21,7 @@ interface DataFetcherInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`.
|
||||
* Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache: remote`.
|
||||
*/
|
||||
export const noCacheFetchOptions: Partial<RequestInit> = {
|
||||
next: {
|
||||
@@ -208,7 +208,7 @@ function cacheLifeFromResponse(
|
||||
}
|
||||
|
||||
const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(`getUserById(${params.userId})`, async () => {
|
||||
const api = apiClient(input);
|
||||
@@ -224,7 +224,7 @@ const getUserById = cache(async (input: DataFetcherInput, params: { userId: stri
|
||||
|
||||
const getSpace = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
@@ -254,7 +254,7 @@ const getSpace = cache(
|
||||
|
||||
const getChangeRequest = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
@@ -286,7 +286,7 @@ const getChangeRequest = cache(
|
||||
|
||||
const getRevision = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; revisionId: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
|
||||
const api = apiClient(input);
|
||||
@@ -313,7 +313,7 @@ const getRevisionPageMarkdown = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; pageId: string }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
|
||||
@@ -352,7 +352,7 @@ const getRevisionPageDocument = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; pageId: string }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
|
||||
@@ -386,7 +386,7 @@ const getRevisionReusableContentDocument = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; reusableContentId: string }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getRevisionReusableContentDocument(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`,
|
||||
@@ -419,7 +419,7 @@ const getRevisionPageByPath = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; path: string }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(
|
||||
`getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`,
|
||||
@@ -446,7 +446,7 @@ const getRevisionPageByPath = cache(
|
||||
|
||||
const getDocument = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => {
|
||||
const api = apiClient(input);
|
||||
@@ -476,7 +476,7 @@ const getComputedDocument = cache(
|
||||
seed: string;
|
||||
}
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
...getComputedContentSourceCacheTags(
|
||||
{
|
||||
@@ -514,7 +514,7 @@ const getComputedDocument = cache(
|
||||
|
||||
const getLatestOpenAPISpecVersionContent = cache(
|
||||
async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
@@ -550,7 +550,7 @@ const getPublishedContentSite = cache(
|
||||
params: { organizationId: string; siteId: string; siteShareKey: string | undefined },
|
||||
_apiVersion: string
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
@@ -592,7 +592,7 @@ const getSiteRedirectBySource = cache(
|
||||
source: string;
|
||||
}
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
@@ -627,7 +627,7 @@ const getSiteRedirectBySource = cache(
|
||||
|
||||
const getEmbedByUrl = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
@@ -660,7 +660,7 @@ const searchSiteContent = cache(
|
||||
input: DataFetcherInput,
|
||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
@@ -700,7 +700,7 @@ const renderIntegrationUi = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { integrationName: string; request: RenderIntegrationUI }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
@@ -734,7 +734,7 @@ const listRevisionPageMetaLinks = cache(
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; pageId: string }
|
||||
) => {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
return wrapCacheDataFetcherError(async () => {
|
||||
return trace(
|
||||
`listRevisionPageMetaLinks(${params.spaceId}, ${params.revisionId}, ${params.pageId})`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GitBookAPIError } from '@gitbook/api';
|
||||
import { parse as parseCacheControl } from '@tusbar/cache-control';
|
||||
import { unstable_cacheLife as cacheLife } from 'next/cache';
|
||||
import { cacheLife } from 'next/cache';
|
||||
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
|
||||
|
||||
export class DataFetcherError extends Error {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { DataFetcherError, noCacheFetchOptions } from '@/lib/data';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
|
||||
import { cacheLife, cacheTag } from 'next/cache';
|
||||
import { assert } from 'ts-essentials';
|
||||
import { enrichFilesystem } from './enrich';
|
||||
import type {
|
||||
@@ -83,7 +83,7 @@ async function fetchFilesystem(
|
||||
};
|
||||
}
|
||||
> {
|
||||
'use cache';
|
||||
'use cache: remote';
|
||||
try {
|
||||
cacheTag(getCacheTag({ tag: 'space', space: spaceId }));
|
||||
return await fetchFilesystemNoCache(url);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
@@ -32,7 +32,8 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"types/**/*.d.ts",
|
||||
"@gitbook/browser-types"
|
||||
"@gitbook/browser-types",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user