diff --git a/.changeset/sour-spiders-bow.md b/.changeset/sour-spiders-bow.md new file mode 100644 index 000000000..9ae6199e3 --- /dev/null +++ b/.changeset/sour-spiders-bow.md @@ -0,0 +1,5 @@ +--- +'@gitbook/cache-tags': minor +--- + +Export function `getCacheTagForURL` to easily get the cache tag for a URL. diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index dafd32db3..8373aff0d 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -44,6 +44,7 @@ runs: GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL + GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY diff --git a/.github/composite/deploy-vercel/action.yaml b/.github/composite/deploy-vercel/action.yaml index 4d803be94..e097bf774 100644 --- a/.github/composite/deploy-vercel/action.yaml +++ b/.github/composite/deploy-vercel/action.yaml @@ -49,6 +49,7 @@ runs: GITBOOK_SECRET: ${{ inputs.opItem }}/GITBOOK_SECRET GITBOOK_APP_URL: ${{ inputs.opItem }}/GITBOOK_APP_URL GITBOOK_API_URL: ${{ inputs.opItem }}/GITBOOK_API_URL + GITBOOK_API_PUBLIC_URL: ${{ inputs.opItem }}/GITBOOK_API_PUBLIC_URL GITBOOK_API_TOKEN: ${{ inputs.opItem }}/GITBOOK_API_TOKEN GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index aeeb02c9a..000000000 --- a/.prettierignore +++ /dev/null @@ -1,17 +0,0 @@ -.next -.vercel -.open-next - -# Generated -packages/emoji-codepoints/index.ts -packages/gitbook/public/~gitbook/static/ -packages/icons/src/data/*.json - -# Build files -dist/ - -README.md -CHANGELOG.md - -# v2 -packages/gitbook-v2/public/~gitbook/static/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 581887eed..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "printWidth": 100, - "singleQuote": true, - "tabWidth": 4 -} diff --git a/biome.json b/biome.json index 3265cf6eb..0ccfb6e30 100644 --- a/biome.json +++ b/biome.json @@ -18,7 +18,9 @@ "**/.vercel/**/*", "**/.cache/**/*", "**/.wrangler/**/*", - "packages/openapi-parser/src/fixtures/**/*" + "packages/openapi-parser/src/fixtures/**/*", + "packages/emoji-codepoints/index.ts", + "packages/icons/src/data/*.json" ] }, "formatter": { diff --git a/bun.lock b/bun.lock index 4ddcc0537..925d74d21 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "name": "@gitbook/cache-tags", "version": "0.1.0", "dependencies": { - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "assert-never": "^1.2.1", }, "devDependencies": { @@ -49,9 +49,9 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.7.2", + "version": "0.7.3", "dependencies": { - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -134,7 +134,7 @@ "name": "gitbook-v2", "version": "0.2.0", "dependencies": { - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "@gitbook/cache-tags": "workspace:*", "@sindresorhus/fnv1a": "^3.1.0", "next": "^15.2.1", @@ -144,7 +144,7 @@ "warn-once": "^0.1.1", }, "devDependencies": { - "@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d", + "@opennextjs/cloudflare": "^0.5.10", "gitbook": "*", "postcss": "^8", "tailwindcss": "^3.4.0", @@ -152,7 +152,7 @@ }, "packages/icons": { "name": "@gitbook/icons", - "version": "0.1.0", + "version": "0.2.0", "bin": { "gitbook-icons": "./bin/gitbook-icons.js", }, @@ -194,9 +194,9 @@ }, "packages/react-contentkit": { "name": "@gitbook/react-contentkit", - "version": "0.6.1", + "version": "0.6.2", "dependencies": { - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -226,7 +226,7 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.1.2", + "version": "1.1.3", "dependencies": { "@gitbook/openapi-parser": "workspace:*", "@scalar/api-client-react": "^1.1.36", @@ -254,6 +254,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", + "@gitbook/api": "0.97.0", "react": "18.3.1", "react-dom": "18.3.1", }, @@ -626,7 +627,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.96.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-E1hpaFNFoEyc8E4rIQyC6UD3kErGXAI0lKt04zYQQYOBLjRIQ57D4oNIIJBjWn4hHeSxRxWvW3NcxzvOGMmIqQ=="], + "@gitbook/api": ["@gitbook/api@0.97.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-bBTy7nprwlwTW/v8S6A9wGFWatU+F33232xF5Tt4GmSBTZSrAKha5wD205Hr+618va+/zipA8o4hdAGPyUNj0A=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], @@ -792,7 +793,7 @@ "@opennextjs/aws": ["@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@756", { "dependencies": { "@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" }, "bin": { "open-next": "./dist/index.js" } }], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@756", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.111.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@0.5.10", "", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@756", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.111.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-L/D472YT5OW1LwpFtD/aVXHJYcVPbFVX7XdphlUjCR4+2osSQIDnsuNgfDRydHMDJZMKxeZDc251ZBzUVKpCqw=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], diff --git a/package.json b/package.json index 8f0899481..0aa06b439 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "overrides": { "@codemirror/state": "6.4.1", "react": "18.3.1", - "react-dom": "18.3.1" + "react-dom": "18.3.1", + "@gitbook/api": "0.97.0" }, "private": true, "scripts": { diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index 3a55b85cb..abe57873e 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -10,7 +10,7 @@ }, "version": "0.1.0", "dependencies": { - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/cache-tags/src/index.ts b/packages/cache-tags/src/index.ts index 8c8701f0b..b56155b35 100644 --- a/packages/cache-tags/src/index.ts +++ b/packages/cache-tags/src/index.ts @@ -111,6 +111,17 @@ export function getCacheTag( } } +/** + * Get the cache tag for a given URL. + */ +export function getCacheTagForURL(url: string | URL) { + const parsedURL = url instanceof URL ? url : new URL(url); + return getCacheTag({ + tag: 'url', + hostname: parsedURL.hostname, + }); +} + /** * Get the tags for a computed content source. */ diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 9ddf65e2d..b42a06a7e 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -6,7 +6,7 @@ "next": "^15.2.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "@gitbook/api": "0.96.1", + "@gitbook/api": "*", "@gitbook/cache-tags": "workspace:*", "@sindresorhus/fnv1a": "^3.1.0", "server-only": "^0.0.1", @@ -14,7 +14,7 @@ }, "devDependencies": { "gitbook": "*", - "@opennextjs/cloudflare": "https://pkg.pr.new/opennextjs/opennextjs-cloudflare/@opennextjs/cloudflare@236c84d", + "@opennextjs/cloudflare": "^0.5.10", "tailwindcss": "^3.4.0", "postcss": "^8" }, @@ -26,6 +26,7 @@ "start": "next start", "build:v2:cloudflare": "opennextjs-cloudflare", "dev:v2:cloudflare": "wrangler dev --port 8771", - "unit": "bun test" + "unit": "bun test", + "typecheck": "tsc --noEmit" } } diff --git a/packages/gitbook-v2/src/app/~gitbook/env/route.ts b/packages/gitbook-v2/src/app/~gitbook/env/route.ts index e00a918ce..18974ca30 100644 --- a/packages/gitbook-v2/src/app/~gitbook/env/route.ts +++ b/packages/gitbook-v2/src/app/~gitbook/env/route.ts @@ -1,6 +1,7 @@ import { type NextRequest, NextResponse } from 'next/server'; import { + GITBOOK_API_PUBLIC_URL, GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_APP_URL, @@ -22,6 +23,7 @@ export async function GET(_req: NextRequest) { GITBOOK_URL, GITBOOK_APP_URL, GITBOOK_API_URL, + GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL, GITBOOK_USER_AGENT, diff --git a/packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts b/packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts index 9d30787b3..b4e3be3c8 100644 --- a/packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts +++ b/packages/gitbook-v2/src/app/~space/[spaceId]/pdf.ts @@ -4,7 +4,6 @@ import { fetchSpaceContextByIds, } from '@v2/lib/context'; import { createDataFetcher } from '@v2/lib/data'; -import { GITBOOK_API_URL } from '@v2/lib/env'; import { createLinker } from '@v2/lib/links'; import { getAPITokenFromMiddleware } from '@v2/lib/middleware'; @@ -26,7 +25,6 @@ export async function getSpacePDFContext( }); const dataFetcher = createDataFetcher({ apiToken: apiToken, - apiEndpoint: GITBOOK_API_URL, }); const baseContext: GitBookBaseContext = { diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index 7e9927c95..e3f96b352 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -16,7 +16,7 @@ import type { import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data'; import { redirect } from 'next/navigation'; import { assert } from 'ts-essentials'; -import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env'; +import { GITBOOK_URL } from './env'; import { type ImageResizer, createImageResizer } from './images'; import { type GitBookSpaceLinker, createLinker } from './links'; @@ -113,8 +113,7 @@ export function getBaseContext(input: { const urlMode = input.urlMode; const dataFetcher = createDataFetcher({ - apiToken: input.apiToken ?? GITBOOK_API_TOKEN, - apiEndpoint: GITBOOK_API_URL, + apiToken: input.apiToken ?? null, }); const linker = getLinkerForSiteURL({ diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index b8cb1f5db..d69ea17da 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -1,42 +1,40 @@ -import { type ComputedContentSource, GitBookAPI } from '@gitbook/api'; -import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; +import { trace } from '@/lib/tracing'; +import { + type ComputedContentSource, + GitBookAPI, + type GitBookAPIServiceBinding, +} from '@gitbook/api'; +import { + getCacheTag, + getCacheTagForURL, + 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 { wrapDataFetcherError } from './errors'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { - /** - * API host to use. - */ - apiEndpoint: string; - /** * API token. */ apiToken: string | null; } -const commonInput: DataFetcherInput = { - apiEndpoint: GITBOOK_API_URL, - apiToken: GITBOOK_API_TOKEN, -}; - /** * Create a data fetcher using an API token. * The data are being cached by Next.js built-in cache. */ -export function createDataFetcher(input: DataFetcherInput = commonInput): GitBookDataFetcher { +export function createDataFetcher( + input: DataFetcherInput = { apiToken: null } +): GitBookDataFetcher { return { - apiEndpoint: input.apiEndpoint, - async api() { - return getAPI(input); + return apiClient(input); }, withToken({ apiToken }) { return createDataFetcher({ - ...input, apiToken, }); }, @@ -45,94 +43,120 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo // API that are tied to the token // getPublishedContentSite(params) { - return getPublishedContentSite(input, { - organizationId: params.organizationId, - siteId: params.siteId, - siteShareKey: params.siteShareKey, - }); + return trace('getPublishedContentSite', () => + getPublishedContentSite(input, { + organizationId: params.organizationId, + siteId: params.siteId, + siteShareKey: params.siteShareKey, + }) + ); }, getSiteRedirectBySource(params) { - return getSiteRedirectBySource(input, { - organizationId: params.organizationId, - siteId: params.siteId, - siteShareKey: params.siteShareKey, - source: params.source, - }); + return trace('getSiteRedirectBySource', () => + getSiteRedirectBySource(input, { + organizationId: params.organizationId, + siteId: params.siteId, + siteShareKey: params.siteShareKey, + source: params.source, + }) + ); }, getRevision(params) { - return getRevision(input, { - spaceId: params.spaceId, - revisionId: params.revisionId, - metadata: params.metadata, - }); + return trace('getRevision', () => + getRevision(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + metadata: params.metadata, + }) + ); }, getRevisionPages(params) { - return getRevisionPages(input, { - spaceId: params.spaceId, - revisionId: params.revisionId, - metadata: params.metadata, - }); + return trace('getRevisionPages', () => + getRevisionPages(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + metadata: params.metadata, + }) + ); }, getRevisionFile(params) { - return getRevisionFile(input, { - spaceId: params.spaceId, - revisionId: params.revisionId, - fileId: params.fileId, - }); + return trace('getRevisionFile', () => + getRevisionFile(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + fileId: params.fileId, + }) + ); }, getRevisionPageByPath(params) { - return getRevisionPageByPath(input, { - spaceId: params.spaceId, - revisionId: params.revisionId, - path: params.path, - }); + return trace('getRevisionPageByPath', () => + getRevisionPageByPath(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + path: params.path, + }) + ); }, getReusableContent(params) { - return getReusableContent(input, { - spaceId: params.spaceId, - revisionId: params.revisionId, - reusableContentId: params.reusableContentId, - }); + return trace('getReusableContent', () => + getReusableContent(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + reusableContentId: params.reusableContentId, + }) + ); }, getLatestOpenAPISpecVersionContent(params) { - return getLatestOpenAPISpecVersionContent(input, { - organizationId: params.organizationId, - slug: params.slug, - }); + return trace('getLatestOpenAPISpecVersionContent', () => + getLatestOpenAPISpecVersionContent(input, { + organizationId: params.organizationId, + slug: params.slug, + }) + ); }, getSpace(params) { - return getSpace(input, { - spaceId: params.spaceId, - shareKey: params.shareKey, - }); + return trace('getSpace', () => + getSpace(input, { + spaceId: params.spaceId, + shareKey: params.shareKey, + }) + ); }, getChangeRequest(params) { - return getChangeRequest(input, { - spaceId: params.spaceId, - changeRequestId: params.changeRequestId, - }); + return trace('getChangeRequest', () => + getChangeRequest(input, { + spaceId: params.spaceId, + changeRequestId: params.changeRequestId, + }) + ); }, getDocument(params) { - return getDocument(input, { - spaceId: params.spaceId, - documentId: params.documentId, - }); + return trace('getDocument', () => + getDocument(input, { + spaceId: params.spaceId, + documentId: params.documentId, + }) + ); }, getComputedDocument(params) { - return getComputedDocument(input, { - organizationId: params.organizationId, - spaceId: params.spaceId, - source: params.source, - }); + return trace('getComputedDocument', () => + getComputedDocument(input, { + organizationId: params.organizationId, + spaceId: params.spaceId, + source: params.source, + }) + ); }, getEmbedByUrl(params) { - return getEmbedByUrl(input, { - url: params.url, - spaceId: params.spaceId, - }); + return trace('getEmbedByUrl', () => + getEmbedByUrl(input, { + url: params.url, + spaceId: params.spaceId, + }) + ); }, searchSiteContent(params) { - return searchSiteContent(input, params); + return trace('searchSiteContent', () => searchSiteContent(input, params)); }, // @@ -140,25 +164,31 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo // where the data is the same for all users // getUserById(userId) { - return getUserById(commonInput, userId); + return trace('getUserById', () => getUserById({ apiToken: null }, { userId })); }, getPublishedContentByUrl(params) { - return getPublishedContentByUrl(commonInput, { - url: params.url, - visitorAuthToken: params.visitorAuthToken, - redirectOnError: params.redirectOnError, - }); + return trace('getPublishedContentByUrl', () => + getPublishedContentByUrl( + { apiToken: null }, + { + url: params.url, + visitorAuthToken: params.visitorAuthToken, + redirectOnError: params.redirectOnError, + } + ) + ); }, }; } -async function getUserById(input: DataFetcherInput, userId: string) { +async function getUserById(input: DataFetcherInput, params: { userId: string }) { 'use cache'; cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).users.getUserById(userId); + const api = await apiClient(input); + const res = await api.users.getUserById(params.userId); return res.data; }); } @@ -181,7 +211,8 @@ async function getSpace( ); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getSpaceById(params.spaceId, { + const api = await apiClient(input); + const res = await api.spaces.getSpaceById(params.spaceId, { shareKey: params.shareKey, }); return res.data; @@ -200,10 +231,8 @@ async function getChangeRequest( cacheLife('minutes'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId - ); + const api = await apiClient(input); + const res = await api.spaces.getChangeRequestById(params.spaceId, params.changeRequestId); cacheTag( getCacheTag({ tag: 'change-request', @@ -228,7 +257,8 @@ async function getRevision( cacheLife('max'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, { + const api = await apiClient(input); + const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, { metadata: params.metadata, }); return res.data; @@ -248,13 +278,10 @@ async function getRevisionPages( cacheLife('max'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, - } - ); + const api = await apiClient(input); + const res = await api.spaces.listPagesInRevisionById(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); return res.data.pages; }); } @@ -272,10 +299,12 @@ async function getRevisionFile( cacheLife('max'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getFileInRevisionById( + const api = await apiClient(input); + const res = await api.spaces.getFileInRevisionById( params.spaceId, params.revisionId, - params.fileId + params.fileId, + {} ); return res.data; }); @@ -295,10 +324,12 @@ async function getRevisionPageByPath( const encodedPath = encodeURIComponent(params.path); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getPageInRevisionByPath( + const api = await apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( params.spaceId, params.revisionId, - encodedPath + encodedPath, + {} ); return res.data; @@ -317,7 +348,8 @@ async function getDocument( cacheLife('max'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId); + const api = await apiClient(input); + const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {}); return res.data; }); } @@ -345,7 +377,8 @@ async function getComputedDocument( ); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, { + const api = await apiClient(input); + const res = await api.spaces.getComputedDocument(params.spaceId, { source: params.source, }); return res.data; @@ -365,7 +398,8 @@ async function getReusableContent( cacheLife('max'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).spaces.getReusableContentInRevisionById( + const api = await apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( params.spaceId, params.revisionId, params.reusableContentId @@ -393,7 +427,8 @@ async function getLatestOpenAPISpecVersionContent( cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent( + const api = await apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( params.organizationId, params.slug ); @@ -413,17 +448,12 @@ async function getPublishedContentByUrl( const { url, visitorAuthToken, redirectOnError } = params; - const hostname = new URL(url).hostname; - cacheTag( - getCacheTag({ - tag: 'url', - hostname, - }) - ); + cacheTag(getCacheTagForURL(url)); cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).urls.getPublishedContentByUrl({ + const api = await apiClient(input); + const res = await api.urls.getPublishedContentByUrl({ url, visitorAuthToken: visitorAuthToken ?? undefined, redirectOnError, @@ -452,22 +482,19 @@ async function getPublishedContentSite( ) { 'use cache'; + cacheLife('days'); cacheTag( getCacheTag({ tag: 'site', site: params.siteId, }) ); - cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - } - ); + const api = await apiClient(input); + const res = await api.orgs.getPublishedContentSite(params.organizationId, params.siteId, { + shareKey: params.siteShareKey, + }); return res.data; }); } @@ -492,14 +519,11 @@ async function getSiteRedirectBySource( cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, - } - ); + const api = await apiClient(input); + const res = await api.orgs.getSiteRedirectBySource(params.organizationId, params.siteId, { + shareKey: params.siteShareKey, + source: params.source, + }); return res.data; }); @@ -517,7 +541,7 @@ async function getEmbedByUrl( cacheLife('weeks'); return wrapDataFetcherError(async () => { - const api = getAPI(input); + const api = await apiClient(input); const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url }); return res.data; }); @@ -534,7 +558,8 @@ async function searchSiteContent( cacheLife('days'); return wrapDataFetcherError(async () => { - const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, { + const api = await apiClient(input); + const res = await api.orgs.searchSiteContent(organizationId, siteId, { query, ...scope, }); @@ -542,12 +567,43 @@ async function searchSiteContent( }); } -function getAPI(input: DataFetcherInput) { - const { apiEndpoint, apiToken } = input; +let loggedServiceBinding = false; + +/** + * Create a new API client. + */ +export async function apiClient(input: DataFetcherInput = { apiToken: null }) { + const { apiToken } = input; + let serviceBinding: GitBookAPIServiceBinding | undefined; + + try { + // HACK: This is a workaround to avoid webpack trying to bundle this cloudflare only module + // @ts-ignore + const { env } = await import( + /* webpackIgnore: true */ `${'__cloudflare:workers'.replaceAll('_', '')}` + ); + serviceBinding = env.GITBOOK_API; + 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})`); + } + } + } catch (error) { + if (process.env.NODE_ENV === 'production' && !process.env.VERCEL) { + throw error; + } + } + const api = new GitBookAPI({ - authToken: apiToken ?? undefined, - endpoint: apiEndpoint, + authToken: apiToken || GITBOOK_API_TOKEN || undefined, + endpoint: GITBOOK_API_URL, userAgent: GITBOOK_USER_AGENT, + serviceBinding, }); return api; diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts index 03914b176..5ae26a267 100644 --- a/packages/gitbook-v2/src/lib/data/lookup.ts +++ b/packages/gitbook-v2/src/lib/data/lookup.ts @@ -1,7 +1,8 @@ import { race, tryCatch } from '@/lib/async'; import { joinPath } from '@/lib/paths'; -import { GitBookAPI, type PublishedSiteContentLookup } from '@gitbook/api'; -import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; +import { trace } from '@/lib/tracing'; +import type { PublishedSiteContentLookup } from '@gitbook/api'; +import { apiClient } from './api'; import { getExposableError } from './errors'; import type { DataFetcherResponse } from './types'; import { getURLLookupAlternatives, stripURLSearch } from './urls'; @@ -20,34 +21,26 @@ export async function getPublishedContentByURL(input: { const lookup = getURLLookupAlternatives(url); const result = await race(lookup.urls, async (alternative, { signal }) => { - const api = new GitBookAPI({ - authToken: GITBOOK_API_TOKEN ?? undefined, - endpoint: GITBOOK_API_URL, - userAgent: GITBOOK_USER_AGENT, - }); + const api = await apiClient(); - const startTime = performance.now(); - const callResult = await tryCatch( - api.urls.getPublishedContentByUrl( - { - url: alternative.url, - visitorAuthToken: input.visitorAuthToken ?? undefined, - redirectOnError: input.redirectOnError, - cache: true, - }, - { - signal, - headers: { - 'x-gitbook-force-cache': 'true', - }, - } - ) - ); - const endTime = performance.now(); - - // biome-ignore lint/suspicious/noConsole: we want to log performance data - console.log( - `getPublishedContentByURL(${alternative.url}) Time taken: ${endTime - startTime}ms` + const callResult = await trace( + { + operation: 'getPublishedContentByURL', + name: alternative.url, + }, + () => + tryCatch( + api.urls.getPublishedContentByUrl( + { + url: alternative.url, + visitorAuthToken: input.visitorAuthToken ?? undefined, + redirectOnError: input.redirectOnError, + }, + { + signal, + } + ) + ) ); if (callResult.error) { diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 166909591..ef6cff4ee 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -20,11 +20,6 @@ export type DataFetcherResponse = * It is used between v1 and v2. */ export interface GitBookDataFetcher { - /** - * Endpoint of the API. - */ - apiEndpoint: string; - /** * Get an API client for the current context. */ diff --git a/packages/gitbook-v2/src/lib/env/globals.ts b/packages/gitbook-v2/src/lib/env/globals.ts index b6e50732a..cb34b78d6 100644 --- a/packages/gitbook-v2/src/lib/env/globals.ts +++ b/packages/gitbook-v2/src/lib/env/globals.ts @@ -34,6 +34,11 @@ export const GITBOOK_APP_URL = process.env.GITBOOK_APP_URL || 'https://app.gitbo */ export const GITBOOK_API_URL = process.env.GITBOOK_API_URL || 'https://api.gitbook.com'; +/** + * Default GitBook API URL endpoint, to be shared with the client. + */ +export const GITBOOK_API_PUBLIC_URL = process.env.GITBOOK_API_PUBLIC_URL || GITBOOK_API_URL; + /** * Default GitBook API token. * It can be use to avoid rate-limiting. diff --git a/packages/gitbook-v2/wrangler.toml b/packages/gitbook-v2/wrangler.toml index 5f18d1545..86e45433b 100644 --- a/packages/gitbook-v2/wrangler.toml +++ b/packages/gitbook-v2/wrangler.toml @@ -1,7 +1,7 @@ main = ".open-next/worker.js" name = "gitbook-open-v2" -compatibility_date = "2024-09-23" -compatibility_flags = ["nodejs_compat"] +compatibility_date = "2025-03-11" +compatibility_flags = ["nodejs_compat", "allow_importable_env"] assets = { directory = ".open-next/assets", binding = "ASSETS" } observability = { enabled = true } @@ -12,6 +12,10 @@ kv_namespaces = [ d1_databases = [ { binding = "NEXT_CACHE_D1", database_id = "f59ddb40-ad72-4312-9395-0ac6a129af8e", database_name = "gitbook-open-v2-tags-preview" } ] +services = [ + { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-preview" }, + { binding = "GITBOOK_API", service = "gitbook-x-prod-api-cache" } +] [env.staging] routes = [ @@ -24,6 +28,10 @@ kv_namespaces = [ d1_databases = [ { binding = "NEXT_CACHE_D1", database_id = "9df62e39-1f35-4066-83aa-e9b8ed3ac8d5", database_name = "gitbook-open-v2-tags-staging" } ] +services = [ + { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-staging" }, + { binding = "GITBOOK_API", service = "gitbook-x-staging-api-cache" } +] [env.production] routes = [ @@ -36,3 +44,7 @@ kv_namespaces = [ d1_databases = [ { binding = "NEXT_CACHE_D1", database_id = "a6f16fce-5f45-43a9-89a4-7b83ddf25b77", database_name = "gitbook-open-v2-tags-production" } ] +services = [ + { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-production" }, + { binding = "GITBOOK_API", service = "gitbook-x-prod-api-cache" } +] diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index acc7ccef5..f90f20a94 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -2,7 +2,7 @@ import { argosScreenshot } from '@argos-ci/playwright'; import { CustomizationBackground, CustomizationCorners, - CustomizationFont, + CustomizationDefaultFont, type CustomizationHeaderItem, CustomizationHeaderPreset, CustomizationIconsStyle, @@ -249,7 +249,7 @@ export function getCustomizationURL(partial: DeepPartial - getCacheTag({ - tag: 'url', - hostname: new URL(url).hostname, - }), + tag: (url) => getCacheTagForURL(url), get: async ( url: string, visitorAuthToken: string | undefined, diff --git a/packages/gitbook/src/lib/async.test.ts b/packages/gitbook/src/lib/async.test.ts index 23b8ad5bb..8d371e88a 100644 --- a/packages/gitbook/src/lib/async.test.ts +++ b/packages/gitbook/src/lib/async.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'bun:test'; -import { asyncMutexFunction, race } from './async'; +import { race } from './async'; import { flushWaitUntil } from './waitUntil'; describe('race', () => { @@ -380,88 +380,3 @@ describe('race', () => { }); }); }); - -describe('asyncMutexFunction', () => { - describe('self', () => { - it('should run only once', async () => { - let running = 0; - - const fn = async () => { - running += 1; - - await new Promise((resolve) => setTimeout(resolve, 10)); - }; - - const mutexFn = asyncMutexFunction(); - - await Promise.all([ - mutexFn(fn), - mutexFn(fn), - mutexFn(fn), - mutexFn(fn), - mutexFn(fn), - mutexFn(fn), - ]); - - expect(running).toBe(1); - }); - }); - - describe('runBlocking', () => { - it('should run only one function at a time', async () => { - let running = 0; - let maxRunning = 0; - - const fn = async () => { - running += 1; - maxRunning = Math.max(running, maxRunning); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - running -= 1; - }; - - const mutexFn = asyncMutexFunction(); - - await Promise.all([ - mutexFn.runBlocking(fn), - mutexFn.runBlocking(fn), - mutexFn.runBlocking(fn), - mutexFn.runBlocking(fn), - mutexFn.runBlocking(fn), - mutexFn.runBlocking(fn), - ]); - - expect(maxRunning).toBe(1); - }); - }); - - describe('runAfter', () => { - it('should run only one function at a time', async () => { - let running = 0; - let maxRunning = 0; - - const fn = async () => { - running += 1; - maxRunning = Math.max(running, maxRunning); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - running -= 1; - }; - - const mutexFn = asyncMutexFunction(); - - await Promise.all([ - mutexFn.runAfter(fn), - mutexFn.runAfter(fn), - mutexFn.runAfter(fn), - mutexFn.runAfter(fn), - mutexFn.runAfter(fn), - mutexFn.runAfter(fn), - ]); - - expect(maxRunning).toBe(1); - }); - }); -}); diff --git a/packages/gitbook/src/lib/async.ts b/packages/gitbook/src/lib/async.ts index 15c60a577..fd77eda37 100644 --- a/packages/gitbook/src/lib/async.ts +++ b/packages/gitbook/src/lib/async.ts @@ -388,87 +388,6 @@ export type AsyncMutexFunction = ((fn: () => Promise) => Promise) & { ) => Promise; }; -/** - * Creates a function that will only call the given function once at a time. - */ -export function asyncMutexFunction(): AsyncMutexFunction { - let pending: - | undefined - | { kind: 'value'; promise: Promise } - | { kind: 'blocking'; promise: Promise }; - - const mutex: AsyncMutexFunction = async (fn) => { - if (pending?.kind === 'value') { - return pending.promise; - } - - while (pending) { - await pending.promise; - } - - const promise = fn(); - pending = { kind: 'value', promise }; - try { - const result = await promise; - return result; - } finally { - pending = undefined; - } - }; - - mutex.wait = async () => { - return pending?.promise; - }; - - mutex.runBlocking = async (fn, options) => { - const failOnMutexError = options?.failOnMutexError ?? true; - - while (pending) { - try { - await pending.promise; - } catch (err) { - if (failOnMutexError) { - throw err; - } - } - } - - const promise = fn(); - pending = { kind: 'blocking', promise }; - try { - const result = await promise; - return result; - } finally { - pending = undefined; - } - }; - - mutex.runAfter = async (fn, options) => { - const failOnMutexError = options?.failOnMutexError ?? true; - - while (pending) { - try { - await pending.promise; - } catch (err) { - if (failOnMutexError) { - throw err; - } - } - } - - const promise = fn(); - pending = { kind: 'value', promise }; - try { - const result = await pending.promise; - return result; - } finally { - pending = undefined; - } - }; - - return mutex; -} - /** * Try catch a promise and return the result or the error. */ diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts index c2709bfa2..d8883153c 100644 --- a/packages/gitbook/src/lib/tracing.ts +++ b/packages/gitbook/src/lib/tracing.ts @@ -1,5 +1,3 @@ -import { startSpan } from '@sentry/nextjs'; - export interface TraceSpan { setAttribute: (label: string, value: boolean | string | number) => void; } @@ -22,35 +20,26 @@ export async function trace( typeof name === 'string' ? { operation: name, name: undefined } : name; const completeName = executionName ? `${operation}(${executionName})` : operation; - return await startSpan( - { - name: completeName, - op: operation, + const attributes: Record = {}; + const span: TraceSpan = { + setAttribute(label, value) { + attributes[label] = value; }, - async (sentrySpan) => { - const attributes: Record = {}; - const span: TraceSpan = { - setAttribute(label, value) { - attributes[label] = value; - sentrySpan?.setAttribute(label, value); - }, - }; + }; - const start = now(); - try { - return await fn(span); - } catch (error) { - span.setAttribute('error', true); - throw error; - } finally { - if (process.env.SILENT !== 'true') { - const end = now(); - // biome-ignore lint/suspicious/noConsole: we want to log performance data - console.log(`trace ${completeName} ${end - start}ms`, attributes); - } - } + const start = now(); + try { + return await fn(span); + } catch (error) { + span.setAttribute('error', true); + throw error; + } finally { + if (process.env.SILENT !== 'true') { + const end = now(); + // biome-ignore lint/suspicious/noConsole: we want to log performance data + console.log(`trace ${completeName} ${end - start}ms`, attributes); } - ); + } } /** diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts index 4468f5216..6b148a60e 100644 --- a/packages/gitbook/src/lib/utils.ts +++ b/packages/gitbook/src/lib/utils.ts @@ -15,7 +15,7 @@ export function defaultCustomizationForSpace(): api.CustomizationSettings { light: '#346DDB', }, corners: api.CustomizationCorners.Rounded, - font: api.CustomizationFont.Inter, + font: api.CustomizationDefaultFont.Inter, background: api.CustomizationBackground.Plain, }, favicon: {}, diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 5544542cd..c47686f23 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -1,4 +1,4 @@ -import { CustomizationFont, CustomizationHeaderPreset } from '@gitbook/api'; +import { CustomizationDefaultFont, CustomizationHeaderPreset } from '@gitbook/api'; import { colorContrast } from '@gitbook/colors'; import { redirect } from 'next/navigation'; import { ImageResponse } from 'next/og'; @@ -8,22 +8,22 @@ import { getAssetURL } from '@/lib/assets'; import { filterOutNullable } from '@/lib/typescript'; import type { GitBookSiteContext } from '@v2/lib/context'; -const googleFontsMap: { [fontName in CustomizationFont]: string } = { - [CustomizationFont.Inter]: 'Inter', - [CustomizationFont.FiraSans]: 'Fira Sans Extra Condensed', - [CustomizationFont.IBMPlexSerif]: 'IBM Plex Serif', - [CustomizationFont.Lato]: 'Lato', - [CustomizationFont.Merriweather]: 'Merriweather', - [CustomizationFont.NotoSans]: 'Noto Sans', - [CustomizationFont.OpenSans]: 'Open Sans', - [CustomizationFont.Overpass]: 'Overpass', - [CustomizationFont.Poppins]: 'Poppins', - [CustomizationFont.Raleway]: 'Raleway', - [CustomizationFont.Roboto]: 'Roboto', - [CustomizationFont.RobotoSlab]: 'Roboto Slab', - [CustomizationFont.SourceSansPro]: 'Source Sans 3', - [CustomizationFont.Ubuntu]: 'Ubuntu', - [CustomizationFont.ABCFavorit]: 'Inter', +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', }; /** diff --git a/packages/gitbook/tests/utils.ts b/packages/gitbook/tests/utils.ts index 05cc3a51b..a5f0466d9 100644 --- a/packages/gitbook/tests/utils.ts +++ b/packages/gitbook/tests/utils.ts @@ -49,18 +49,12 @@ export function getTestURL(urlRest: string): string { * Get an API token for a site by its URL. */ export async function getSiteAPIToken(url: string) { - const api = new GitBookAPI(); - const { data } = await api.urls.getPublishedContentByUrl( - { - url, - cache: true, - }, - { - headers: { - 'x-gitbook-force-cache': 'true', - }, - } - ); + const api = new GitBookAPI({ + endpoint: 'https://api.gitbook.com/cache', + }); + const { data } = await api.urls.getPublishedContentByUrl({ + url, + }); if ('redirect' in data) { throw new Error(`Invalid site URL, it resulted in a redirect: ${data.redirect}`); diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index 9bc654363..e9b34fe53 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.96.1", + "@gitbook/api": "*", "@gitbook/icons": "workspace:*" }, "peerDependencies": { diff --git a/turbo.json b/turbo.json index 418c606cb..e036b8cda 100644 --- a/turbo.json +++ b/turbo.json @@ -37,7 +37,7 @@ }, // Check the package for type errors "typecheck": { - "dependsOn": ["^typecheck", "build", "generate"] + "dependsOn": ["^typecheck", "build"] }, // Lint the package for style errors "lint": {