Compare commits

..

1 Commits

Author SHA1 Message Date
Nolann Biron f2766f3fdc Revert OpenAPI Spec validation 2025-02-25 16:52:06 +01:00
1087 changed files with 24041 additions and 93944 deletions
+1
View File
@@ -0,0 +1 @@
1.2.1
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Assistant: you can now send follow-up questions while an answer is still being written. Each one appears as your own message with a "Queued" badge (hover for when it will send, × to cancel), and they're sent automatically one at a time as each answer completes.
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Remove unused search api method from gitbook/api/lib
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Update OpenAPI code examples to support multiple content-type
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add a carousel layout option to cards blocks, rendering them as a horizontally-scrolling, scroll-snapping row instead of a wrapping grid.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/openapi-parser': major
---
Use Scalar parser to upgrade OpenAPI spec v2
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix the docs embed `navigateToPage` API on multi-space sites. Deep-linking to a page in a different space/section (e.g. `navigateToPage('/help-center/integrations')`) previously 404'd because the section base was not placed before `~gitbook/embed/page`. The target is now resolved to its space server-side, so pages in any space resolve correctly. The input accepts the page path, an absolute path, or the full published URL.
@@ -1,6 +0,0 @@
---
"@gitbook/colors": patch
"gitbook": patch
---
A near-white tint color (e.g. a warm `#F5F3EF`) is now taken as the exact page background, mirroring the existing behavior for near-black tints. The tint's exact lightness, hue and chroma are preserved, and the color is anchored to whichever scale step the active theme renders as the background — so it matches exactly on `muted` (which uses the second step) as well as `clean`. This applies only to near-neutral tints that are light enough to read as a background; saturated or merely light-ish colors keep their normal accent scale. The `bold` theme is unaffected: it already uses the tint for the header and stays intentionally two-tone.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Hide x-gitbook-\* symbols in OpenAPI blocks
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add cover image background mode and masks
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Button blocks now respect the `size` option, so you can render small, medium, or large buttons.
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix circularRef in schema + examples OpenAPI
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": minor
---
Add an `askQuestion` tool to the site MCP server. Alongside `searchDocumentation` and `getPage`, MCP clients can now ask a natural-language question and get a synthesized answer with links to the source pages, powered by the same AI search backend as the site's "ask a question" experience. The tool accepts an optional `goal` param so calling agents can attach the intent they're trying to accomplish, which tailors the answer and is tracked in analytics. The tool is only exposed on sites that have AI enabled.
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Update design for hint block: use semantic colors (info, warning, danger, success) and add alternative styling for hints with headings
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Add support for new OpenAPI ref
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/react-openapi': patch
---
Fix ID not set when there is no operation summary
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Remove GBO's redundant re-selection of the best-scoring search section. The search API now returns a single highest-scoring section per page (and orders sections highest-score-first), so GBO no longer needs its own `getBestScoredResult` helper to pick the best section for the search and MCP previews. No user-visible change.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Sync the API reference responses selector with the "Responses" collapsibles, and keep the selected response in sync across every operation on the page (like the code sample language selector). Selecting a status code now expands the matching response section and applies to all operations at once.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix a light/dark flash on published sites configured to respect the system default (no theme toggle). Such sites forced the `system` theme, but `next-themes`' pre-paint script applies a forced value verbatim without resolving `prefers-color-scheme`, so the page painted light and only switched to dark after hydration. We now leave the theme unforced when the default is `system` (only concrete light/dark themes are forced), letting `next-themes`' existing pre-paint script resolve the system preference before first paint.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Add a `sendFeedback` MCP tool so AI agents can report documentation findings (outdated / incoherent / gap / other) as `agent_feedback` insights events. The tool only accepts finding categories, so it never records positive feedback.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Separate the prompt block actions into a primary "Open in" dropdown and a secondary "Copy prompt" button, instead of a single combined button group, and align the block's design with the expandable block (bordered frame, left disclosure chevron, and subtle elevation when expanded).
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix the docs embed widget shipping a stale script: declare the embed package's `standalone/` bundle as a Turbo build output. Because it wasn't declared, changes confined to the standalone widget (which compiles to `standalone/` but not `dist/`) didn't invalidate the downstream `generate` cache that copies it into the app, so the deployed widget could lag the source — e.g. the `clipboard-write` permission on the widget iframe never reached production, breaking the copy button in the Assistant embed.
+7
View File
@@ -0,0 +1,7 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Render GitBook blocks in OpenAPI operation description
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Refine the per-paragraph AI ask button: shorten its tooltip to "Ask" (from "Ask <assistant> about this"), and hide it inside cards where it would otherwise be clipped by the card's overflow.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Keep the "On this page" and "Ask" buttons pinned below the header while scrolling on desktop API reference pages, so the page outline stays reachable throughout long operations.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Hide unfocusable unlabelled button from screen readers
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Only show the "Back to [space]" shortcut for cross-space links in the table of contents, not for in-content text links or other ways of reaching another space.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
a11y screen reader fixes
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/react-contentkit": patch
"gitbook": patch
---
Let integration block webframes navigate the reader to another page in the site by posting a `@webframe.navigate` action with a `path` (and optional `anchor`). Resolved client-side against the site base path, so navigation stays in-site and drives the standard navigation progress bar.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix an issue where certain keywords could cause an exception when rendering emojis
-12
View File
@@ -1,12 +0,0 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "🚀 Dev server",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "bun install && bun dev"],
"port": 3000,
"autoPort": false
}
]
}
-3
View File
@@ -1,3 +0,0 @@
# Changes to the API data cache functions can invalidate all existing data cache
# causing a massive amount of revalidation, impacting our API.
packages/gitbook/src/lib/data/api.ts @SamyPesse
+6 -28
View File
@@ -53,41 +53,19 @@ After forking this repository, you'll want to [create a branch](https://docs.git
#### 3. Install dependencies and run the project locally
##### Prerequisites:
GitBook uses [Bun](https://bun.sh/) to run the project. Make sure you're using the specified version of `node` before running any of the development commands to ensure a smooth development experience.
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
You can easily do this by running the command `nvm use`.
##### Setup steps:
1. Ensure you are using the project's version of Node:
```bash
nvm use
```
2. Install dependencies using Bun:
```bash
bun install
```
3. Start the development server:
```bash
bun dev
```
Additional development commands:
- `bun format`: Format the code using Biome
- `bun typecheck`: Run TypeScript type checking
- `bun unit`: Run unit tests
- `bun e2e`: Run end-to-end tests
To start your local version of GitBook, run the command `bun dev`.
#### 4. Preview your changes
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
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/url/gitbook.com/docs` after running the development server.
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.
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.
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.
### Commit your update
@@ -1,83 +0,0 @@
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.43.0'
environment: ${{ inputs.environment }}
command: deployments status --config ./packages/gitbook/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.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/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.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/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.43.0'
environment: ${{ inputs.environment }}
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/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 }}"
@@ -0,0 +1,37 @@
name: 'Setup Playwright'
description: 'Install Playwright and dependencies'
runs:
using: 'composite'
steps:
# Run npm ci and get Playwright version
- name: 🏗 Prepare Playwright env
shell: bash
run: |
PLAYWRIGHT_VERSION=$(npm ls --json @playwright/test | jq --raw-output '.dependencies["gitbook"].dependencies["@playwright/test"].version')
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
# Cache browser binaries, cache key is based on Playwright version and OS
- name: 🧰 Cache Playwright browser binaries
uses: actions/cache@v4
id: playwright-cache
with:
path: '~/.cache/ms-playwright'
key: '${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}'
restore-keys: |
${{ runner.os }}-playwright-
# Install browser binaries & OS dependencies if cache missed
- name: 🏗 Install Playwright browser binaries & OS dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
shell: bash
working-directory: packages/gitbook
run: |
bun x playwright install --with-deps chromium
# Install only the OS dependencies if cache hit
- name: 🏗 Install Playwright OS dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
shell: bash
working-directory: packages/gitbook
run: |
bun x playwright install-deps
@@ -1,144 +0,0 @@
name: 'Deploy cloudflare'
description: 'Deploy GitBook to Cloudflare'
inputs:
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
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
deploy:
description: 'Deploy as main version for all traffic instead of uploading versions'
required: true
commitTag:
description: 'Commit branch to associate with the deployment'
required: true
commitMessage:
description: 'Commit message to associate with the deployment'
required: true
outputs:
deployment-url:
description: "Deployment URL"
value: ${{ steps.upload_middleware.outputs.deployment-url }}
runs:
using: 'composite'
steps:
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
shell: bash
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.opItem }}/NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
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_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
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: Build worker
run: bun run turbo build:cloudflare
env:
GITBOOK_RUNTIME: cloudflare
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
shell: bash
- name: Upload the DO worker
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
- id: upload_server
name: Upload server to Cloudflare
uses: cloudflare/wrangler-action@v3.14.0
with:
apiToken: ${{ inputs.apiToken }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/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/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 }}
accountId: ${{ inputs.accountId }}
workingDirectory: ./
wranglerVersion: '4.43.0'
environment: ${{ inputs.environment }}
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/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.upload_middleware.outputs.deployment-url }}
run: |
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
-100
View File
@@ -1,100 +0,0 @@
name: 'Deploy vercel'
description: 'Deploy GitBook to Vercel'
inputs:
vercelOrg:
description: 'Vercel organization'
required: true
vercelProject:
description: 'Vercel project'
required: true
vercelToken:
description: 'Vercel token'
required: true
opItem:
description: '1Password item to load secrets from'
required: true
opServiceAccount:
description: '1Password service account token'
required: true
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"
value: ${{ steps.deploy.outputs.deployment-url }}
runs:
using: 'composite'
steps:
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
shell: bash
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Pull Vercel Environment Information
run: bun run vercel pull --yes --environment=${{ inputs.environment }} --token=${{ inputs.vercelToken }}
shell: bash
env:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Load secret
uses: 1password/load-secrets-action@v2
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ inputs.opServiceAccount }}
GITBOOK_URL: ${{ inputs.opItem }}/GITBOOK_URL
GITBOOK_ICONS_URL: ${{ inputs.opItem }}/GITBOOK_ICONS_URL
GITBOOK_ICONS_TOKEN: ${{ inputs.opItem }}/GITBOOK_ICONS_TOKEN
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_OAUTH_SERVER_URL: ${{ inputs.opItem }}/GITBOOK_OAUTH_SERVER_URL
GITBOOK_PREVIEW_BASE_URL: ${{ inputs.opItem }}/GITBOOK_PREVIEW_BASE_URL
GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST
GITBOOK_INTEGRATIONS_CONTENT_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_CONTENT_HOST
GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY
GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL_2
GITBOOK_IMAGE_RESIZE_SALT: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SALT
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 "GITBOOK_BLOCK_SEARCH_INDEXATION=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE=true" >> .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
env:
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
run: |
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=${{ inputs.environment }} --token=${{ inputs.vercelToken }})
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
env:
VERCEL_ORG_ID: ${{ inputs.vercelOrg }}
VERCEL_PROJECT_ID: ${{ inputs.vercelProject }}
- name: Outputs
shell: bash
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+10 -1
View File
@@ -3,7 +3,16 @@ description: 'Install Bun and cache dependencies'
runs:
using: 'composite'
steps:
# We cache dependencies even if Bun is fast to avoid downloading fontawesome too many times
# and impacting bandwidth there.
- name: Cache bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: 'package.json'
bun-version-file: '.bun-version'
+309 -7
View File
@@ -4,12 +4,301 @@ on:
push:
branches:
- main
- changeset-release/main
env:
NPM_TOKEN_READONLY: ${{ secrets.NPM_TOKEN_READONLY }}
jobs:
deploy:
name: Deploy to Cloudflare Pages
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
deployment-alias-url: ${{ steps.deploy.outputs.deployment-alias-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for production
run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/heads/main')
- name: Sets env vars for preview
run: |
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
if: 1 && !startsWith(github.ref, 'refs/heads/main')
- name: Build Next.js with next-on-pages
run: bun run turbo build:cloudflare
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
SENTRY_RELEASE: ${{ github.sha }}
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.11.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: ./
wranglerVersion: '3.82.0'
command: pages deploy ./packages/gitbook/.vercel/output/static --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME }} --branch=${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
echo "Alias URL: ${{ steps.deploy.outputs.deployment-alias-url }}"
- name: Archive build output
uses: actions/upload-artifact@v4
with:
name: build-output
path: .vercel/
# Until https://github.com/cloudflare/wrangler-action/issues/301 is done
- name: Update Deployment Status to Success
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
-d '{"state": "success", "target_url": "${{ steps.deploy.outputs.deployment-url }}", "description": "Deployed Preview URL for commit", "context": "cloudflare/preview"}' \
https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }}
deploy-v2-vercel:
name: Deploy v2 to Vercel
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for production
run: |
echo "VERCEL_ENVIRONMENT=production" >> $GITHUB_ENV
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/heads/main')
- name: Sets env vars for preview
run: |
echo "VERCEL_ENVIRONMENT=preview" >> $GITHUB_ENV
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
if: 1 && !startsWith(github.ref, 'refs/heads/main')
- name: Pull Vercel Environment Information
run: bun run vercel pull --yes --environment=$VERCEL_ENVIRONMENT --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: bun run vercel build --token=${{ secrets.VERCEL_TOKEN }}
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
SENTRY_RELEASE: ${{ github.sha }}
- name: Deploy Project Artifacts to Vercel
id: deploy
run: |
DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT"
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
issues: write
pull-requests: write
checks: write
statuses: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Sets env vars for production
run: |
echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV
echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV
if: startsWith(github.ref, 'refs/heads/main')
- name: Sets env vars for preview
run: |
echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV
if: 1 && !startsWith(github.ref, 'refs/heads/main')
- name: Build worker
run: bun run turbo build:v2:cloudflare
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
SENTRY_DSN: ${{ vars.SENTRY_DSN }}
SENTRY_RELEASE: ${{ github.sha }}
- id: deploy
name: Deploy to Cloudflare
uses: cloudflare/wrangler-action@v3.11.0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: ./
wranglerVersion: '3.82.0'
command: versions upload --config ./packages/gitbook-v2/wrangler.toml
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
comment-deployments:
runs-on: ubuntu-latest
name: Comment Deployments
if: always() && !startsWith(github.ref, 'refs/heads/main')
needs:
- deploy
- deploy-v2-vercel
- deploy-v2-cloudflare
steps:
- name: Find GitHub Comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: 'Summary of the deployments'
- name: Create or update GitHub comment
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Summary of the deployments:
### Version 1 (production)
| Version | URL | Status |
| --- | --- | --- |
| Latest commit | [${{ needs.deploy.outputs.deployment-url }}](${{ needs.deploy.outputs.deployment-url }}) | ${{ needs.deploy.result == 'success' && '✅' || '❌' }} |
| PR | [${{ needs.deploy.outputs.deployment-alias-url }}](${{ needs.deploy.outputs.deployment-alias-url }}) | ${{ needs.deploy.result == 'success' && '✅' || '❌' }} |
### Version 2 (experimental)
| Version | URL | Status |
| --- | --- | --- |
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
### Test content
| Site | v1 | v2 |
| --- | --- | --- |
| GitBook | [${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/docs.gitbook.com) |
| E2E | [${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/gitbook.gitbook.io/test-gitbook-open) |
edit-mode: replace
visual-testing:
runs-on: ubuntu-latest
name: Visual Testing
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{needs.deploy.outputs.deployment-url}}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-test-results
path: packages/gitbook/test-results/
retention-days: 3
visual-testing-customers:
runs-on: ubuntu-latest
name: Visual Testing Customers
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{needs.deploy.outputs.deployment-url}}
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: 'customers'
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-test-results-customers
path: packages/gitbook/test-results/
retention-days: 3
pagespeed-testing:
runs-on: ubuntu-latest
name: PageSpeed Testing
needs: deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run pagespeed tests
run: bun ./packages/gitbook/tests/pagespeed-testing.ts $DEPLOYMENT_URL
env:
DEPLOYMENT_URL: ${{needs.deploy.outputs.deployment-url}}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
format:
runs-on: ubuntu-latest
name: Format
timeout-minutes: 6
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -20,10 +309,22 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun format:check
lint:
runs-on: ubuntu-latest
name: Lint
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun lint --no-cache
test:
runs-on: ubuntu-latest
name: Test
timeout-minutes: 6
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -34,10 +335,12 @@ jobs:
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- run: bun unit
build:
build-oss:
# CI to check that the repository builds correctly on a machine without the credentials
runs-on: ubuntu-latest
name: Build
timeout-minutes: 6
name: Build (Open Source)
env:
NPM_TOKEN_READONLY: ''
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -51,7 +354,6 @@ jobs:
typecheck:
runs-on: ubuntu-latest
name: Typecheck
timeout-minutes: 6
steps:
- name: Checkout
uses: actions/checkout@v4
-252
View File
@@ -1,252 +0,0 @@
name: Preview
on:
pull_request_target:
push:
branches:
- main
jobs:
approval:
# Ensure that external contributors' PRs are manually approved before deployment
name: Approve Preview Deployment
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.fork }}
environment:
name: preview-approval
steps:
- name: Approval gate
run: echo "Preview deployment approved."
deploy-v2-vercel:
name: Deploy v2 to Vercel (preview)
runs-on: ubuntu-latest
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2v-preview
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Vercel
id: deploy
uses: ./.github/composite/deploy-vercel
with:
environment: preview
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
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
needs: approval
if: ${{ always() && (needs.approval.result == 'success' || needs.approval.result == 'skipped') }}
environment:
name: 2c-preview
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Deploy to Cloudflare
id: deploy
uses: ./.github/composite/deploy-cloudflare
with:
environment: preview
deploy: ${{ github.ref == 'refs/heads/main' }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-preview
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }}
commitMessage: ${{ github.sha }}
- name: Extract Worker ID
id: extract-worker-id
if: ${{ !steps.deploy.outputs.deployment-url }}
run: |
if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then
WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}"
echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT
fi
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}"
comment-deployments:
runs-on: ubuntu-latest
name: Comment Deployments (preview)
if: always() && github.event_name != 'push'
needs:
- deploy-v2-vercel
- deploy-v2-cloudflare
steps:
- name: Find GitHub Comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: 'Summary of the deployments'
- name: Create or update GitHub comment
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Summary of the deployments:
| Version | URL | Status |
| --- | --- | --- |
| Vercel | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}](${{ needs.deploy-v2-vercel.outputs.deployment-url }}) | ${{ needs.deploy-v2-vercel.result == 'success' && '✅' || '❌' }} |
| Cloudflare | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v2-cloudflare.result == 'success' && '✅' || '❌' }} |
### Test content
| Site | `2v` | `2c` |
| --- | --- | --- |
| GitBook | [${{ 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-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-v2-vercel:
runs-on: ubuntu-latest
name: Visual Testing v2
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-vercel
visual-testing-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-cloudflare
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-vercel
visual-testing-customers-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
needs: deploy-v2-cloudflare
if: ${{ always() && needs.deploy-v2-cloudflare.result == 'success' }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run Playwright tests
run: bun e2e-customers
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-cloudflare
browserless-testing-v2-vercel:
runs-on: ubuntu-latest
name: Browserless Testing v2 (Vercel)
needs: deploy-v2-vercel
if: ${{ always() && needs.deploy-v2-vercel.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Run tests
run: cd ./packages/gitbook && bun e2e-browserless
env:
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
# browserless-testing-v2-cloudflare:
# runs-on: ubuntu-latest
# name: Browserless Testing v2 (Cloudflare)
# needs: deploy-v2-cloudflare
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Setup Bun
# uses: ./.github/composite/setup-bun
# - name: Install dependencies
# run: bun install --frozen-lockfile
# env:
# PUPPETEER_SKIP_DOWNLOAD: 1
# - name: Run tests
# run: cd ./packages/gitbook && bun e2e-browserless
# env:
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
-53
View File
@@ -1,53 +0,0 @@
name: Production
on:
push:
branches:
- main
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (production)
runs-on: ubuntu-latest
environment:
name: 2v-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy
id: deploy
uses: ./.github/composite/deploy-vercel
with:
environment: production
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-production
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (production)
runs-on: ubuntu-latest
environment:
name: 2c-production
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy
id: deploy
uses: ./.github/composite/deploy-cloudflare
with:
environment: production
deploy: true
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-production
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: main
commitMessage: ${{ github.sha }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
-53
View File
@@ -1,53 +0,0 @@
name: Staging
on:
push:
branches:
- main
jobs:
deploy-v2-vercel:
name: Deploy v2 to Vercel (staging)
runs-on: ubuntu-latest
environment:
name: 2v-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy
id: deploy
uses: ./.github/composite/deploy-vercel
with:
environment: staging
vercelOrg: ${{ secrets.VERCEL_ORG_ID }}
vercelProject: ${{ secrets.VERCEL_PROJECT_ID }}
vercelToken: ${{ secrets.VERCEL_TOKEN }}
opItem: op://gitbook-open/2v-staging
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
deploy-v2-cloudflare:
name: Deploy v2 to Cloudflare Worker (staging)
runs-on: ubuntu-latest
environment:
name: 2c-staging
url: ${{ steps.deploy.outputs.deployment-url }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Deploy
id: deploy
uses: ./.github/composite/deploy-cloudflare
with:
environment: staging
deploy: true
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
opItem: op://gitbook-open/2c-staging
opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
commitTag: main
commitMessage: ${{ github.sha }}
- name: Outputs
run: |
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
+25 -21
View File
@@ -4,49 +4,53 @@ on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: write
contents: write
env:
# The repo is managed by Bun, but publishing uses npm for provenance.
# Keep Corepack from rejecting packageManager: bun@... when npm runs.
COREPACK_ENABLE_PROJECT_SPEC: 0
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Update npm
run: |
corepack enable npm
corepack install -g npm@latest
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: bun run publish-all-packages
version: bun run changeset-version
publish: npm run release
env:
# Using a PAT instead of GITHUB_TOKEN because we need to run workflows when releases are created
# https://github.com/orgs/community/discussions/26875#discussioncomment-3253761
GITHUB_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
release-preview:
# For now it releases the cache-do to both preview and production
# Once we changed to deploy the app only on release, we should change `release:preview` in `cache-do`
name: Release Preview
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Release preview packages
run: bun run release:preview
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
-4
View File
@@ -25,7 +25,3 @@ yarn-error.log*
# TypeScript
*.tsbuildinfo
.npmrc
# Bun pack artifacts
packages/*/*.tgz
+1 -1
View File
@@ -1 +1 @@
v22.3
v20.6
+14
View File
@@ -0,0 +1,14 @@
.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
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"singleQuote": true,
"tabWidth": 4
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["biomejs.biome"]
}
+1 -11
View File
@@ -7,15 +7,5 @@
["style \\=([^;]*);", "\"([^\"]*)\""],
["style \\=([^;]*);", "\\`([^\\`]*)\\`"]
],
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"],
"prettier.enable": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
"tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"]
}
-81
View File
@@ -1,81 +0,0 @@
# AGENTS Instructions
## Commands
```bash
bun install # Install dependencies
bun dev # Start dev server (all packages)
bun run build # Build all packages
bun run format # Format with Biome (run after every change)
bun run typecheck # Type-check all packages
bun run unit # Run unit tests
```
## Development
The dev server proxies published GitBook sites locally. After `bun dev`, access any published site at:
```
http://localhost:3000/url/<published-gitbook-url>
```
Examples:
- `http://localhost:3000/url/gitbook.com/docs`
- `http://localhost:3000/url/open-source.gitbook.io/midjourney`
## Architecture
```
packages/
gitbook/ # Main Next.js app
src/
app/ # Next.js App Router (sites/)
components/ # React components
lib/ # Server utilities, data fetching
intl/ # Internationalization (translations/)
openapi-parser/ # OpenAPI 3.0/3.1/Swagger parser
react-openapi/ # OpenAPI rendering components
react-contentkit/ # ContentKit component rendering
embed/ # Embeddable GitBook components
shared/ # Shared utilities
icons/ # Icon assets
fonts/ # Font assets
colors/ # Color tokens
expr/ # GitBook expression evaluator
cache-do/ # Cloudflare DO cache
cache-tags/ # Cache tag utilities
```
## Testing
```bash
bun run unit # Unit tests via bun test (not vitest)
bun run e2e # Playwright e2e tests (requires built app)
```
Run a specific test file:
```bash
cd packages/gitbook && bun test src/lib/cache.test.ts
```
## Changesets
After committing code changes, create a changeset for the affected package:
```md
---
"gitbook": patch
---
Provide a short description of the change.
```
Save as `.changeset/<name>.md`, then commit it separately with message: `changeset`
## Formatting
Uses Biome (not ESLint/Prettier). Always run `bun run format` before committing.
## Comments
Comment to explain *why*, not *what* — the code already shows what it does. Keep comments short, ideally a single line. Avoid multi-line block comments that narrate mechanics a reader can follow from the code; they add noise and go stale. Reserve longer comments for genuinely non-obvious rationale: a subtle invariant, or a workaround and the reason it exists.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+32 -15
View File
@@ -1,11 +1,7 @@
<p align="center">
<img src="./assets/gitbook_icon_dark.svg" alt="GitBook" width="48" />
</p>
<h1 align="center">GitBook</h1>
<p align="center">
<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> - <a href="https://github.com/orgs/GitbookIO/discussions/categories/feature-requests">Feature requests</a>
<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>
</p>
<p align="center">
@@ -39,10 +35,10 @@ To run a local version of this project, please follow these simple steps.
### Prerequisites
- Node.js (Version: >= 22.3)
- Use nvm for easy Node management
- [Bun](https://bun.sh/) (Version: >=1.2.15)
- We use a text-based lockfile which isn't supported below 1.2.15
- Node.js (Version: >=20.6)
- Use nvm for easy Node management
- Bun (Version: >=1.2.1)
- We use a text-based lockfile which isn't supported below 1.2.1
### Set up
@@ -66,22 +62,43 @@ bun install
bun dev
```
6. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/url`.
5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`.
examples:
- http://localhost:3000/url/gitbook.com/docs
- http://localhost:3000/url/open-source.gitbook.io/midjourney
- http://localhost:3000/docs.gitbook.com
- http://localhost:3000/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.
### Other development commands
- `bun format`: format the code
- `bun lint`: lint the code
### CI and testing
All pull-requests will be tested against both visual and performances testing to prevent regressions.
## Fonts and Icons
GitBook Open uses fontawesome. For self-hosting and local development, for licensing reasons, only the icons from the free version should be used.
GitBook Open uses fontawesome. During development, your local environment will use the free version. However, only the pro version will be accepted by CI. If you see the following error:
```
The GitBook icon is missing. It indicates that the dependencies were installed without the correct font-awesome package. These changes have probably been persisted in the Bun lockfile. Read the README for more information.
```
It means that you've changed the GBO dependencies and bundled in the free version. Only GitBook staff can help with this - if you're not on the GitBook team, please ping us in the PR and we'll help get things moving.
If you are GitBook staff, you'll need our NPM token in your local environment.
```
.env.local
NPM_TOKEN_READONLY=xxx
```
and then reinstall dependencies.
## Contributing
@@ -133,11 +150,11 @@ See `LICENSE` for more information.
</p>
```md
[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
[![GitBook](https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1)](https://gitbook.com/)
```
```html
<a href="https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link">
<a href="https://gitbook.com">
<img
src="https://img.shields.io/static/v1?message=Documented%20on%20GitBook&logo=gitbook&logoColor=ffffff&label=%20&labelColor=5c5c5c&color=3F89A1"
/>
-3
View File
@@ -1,3 +0,0 @@
<svg width="65" height="65" viewBox="0 0 65 65" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.3963 34.2195C30.5255 36.0255 32.09 36.9285 33.8082 36.93C35.5265 36.9315 37.0926 36.0313 40.2249 34.2308L60.1913 22.7534C61.0927 22.2353 61.6484 21.2749 61.6484 20.2352C61.6484 19.1955 61.0927 18.2351 60.1913 17.717L40.2177 6.23554C37.0888 4.43695 35.5243 3.53766 33.8078 3.53833C32.0912 3.539 30.5275 4.43951 27.4 6.24053L10.2292 16.1286C10.102 16.2019 10.0383 16.2385 9.97905 16.2732C4.11368 19.7068 0.489862 25.9754 0.441408 32.7717C0.440918 32.8404 0.440918 32.9138 0.440918 33.0607C0.440918 33.2074 0.440918 33.2807 0.441407 33.3494C0.489754 40.138 4.10549 46.4008 9.96041 49.837C10.0196 49.8718 10.0831 49.9085 10.2101 49.9818L20.9658 56.1918C27.2331 59.8104 30.3668 61.6197 33.808 61.6208C37.2493 61.622 40.3842 59.8148 46.6539 56.2005L58.008 49.6551C61.1474 47.8454 62.7171 46.9405 63.579 45.4487C64.4409 43.957 64.4409 42.1451 64.4409 38.5215V31.5212C64.4409 30.5159 63.8965 29.5895 63.0182 29.1004C62.1683 28.627 61.1325 28.6341 60.2891 29.1189L37.0074 42.5019C35.4453 43.3998 34.6643 43.8487 33.8072 43.849C32.9501 43.8493 32.1688 43.4008 30.6062 42.5038L14.8487 33.4586C14.0593 33.0055 13.6647 32.7789 13.3477 32.738C12.625 32.6448 11.9301 33.0497 11.6548 33.7244C11.534 34.0203 11.5365 34.4753 11.5414 35.3855C11.545 36.0555 11.5468 36.3905 11.6094 36.6987C11.7496 37.3887 12.1127 38.0136 12.6428 38.4771C12.8795 38.6842 13.1696 38.8516 13.7499 39.1866L30.5974 48.9103C32.164 49.8145 32.9473 50.2666 33.8075 50.2668C34.6677 50.267 35.4512 49.8154 37.0184 48.912L57.6684 37.0085C58.2037 36.7 58.4713 36.5457 58.672 36.6616C58.8727 36.7776 58.8727 37.0865 58.8727 37.7044V40.8796C58.8727 41.7855 58.8727 42.2385 58.6572 42.6114C58.4417 42.9844 58.0493 43.2106 57.2644 43.663L40.2322 53.4811C37.0966 55.2885 35.5288 56.1923 33.8078 56.1915C32.0869 56.1907 30.5199 55.2855 27.386 53.4752L11.4509 44.2701C11.4003 44.2409 11.375 44.2262 11.3514 44.2125C8.0102 42.26 5.94856 38.6882 5.92922 34.8185C5.92909 34.7911 5.92909 34.7619 5.92909 34.7035V31.7889C5.92909 29.6526 7.06686 27.678 8.9151 26.6067C10.5483 25.66 12.5628 25.6582 14.1977 26.6018L27.3963 34.2195Z" fill="#181C1F"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 KiB

After

Width:  |  Height:  |  Size: 622 KiB

-178
View File
@@ -1,178 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": [
"**/node_modules/**/*",
"**/dist/**/*",
"**/build/**/*",
"**/public/**/*",
"**/.next/**/*",
"**/.open-next/**/*",
"**/.turbo/**/*",
"**/.vercel/**/*",
"**/.cache/**/*",
"**/.wrangler/**/*",
"packages/embed/standalone/**/*",
"packages/openapi-parser/src/fixtures/**/*",
"packages/emoji-codepoints/index.ts",
"packages/icons/src/data/*.json",
"packages/gitbook/worker-configuration.d.ts",
"gitbook/tsconfig.json",
"**/*.css"
]
},
"formatter": {
"enabled": true,
"useEditorconfig": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 4,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto",
"bracketSpacing": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"performance": {
"noDelete": "warn"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"complexity": {
"noForEach": "off",
"noUselessFragments": "warn",
"noBannedTypes": "warn"
},
"correctness": {
"noUndeclaredVariables": "error",
"noUnusedVariables": "error",
"useArrayLiterals": "error",
"useHookAtTopLevel": "error",
"noUnusedImports": "error",
"noVoidElementsWithChildren": "warn",
"useJsxKeyInIterable": "warn",
"useExhaustiveDependencies": "warn",
"noUnknownFunction": "warn"
},
"style": {
"noNonNullAssertion": "warn",
"noParameterAssign": "off",
"useThrowOnlyError": "error"
},
"suspicious": {
"noConsole": {
"level": "warn",
"options": {
"allow": ["assert", "error", "warn"]
}
},
"noExplicitAny": "warn",
"noImplicitAnyLet": "warn",
"noConfusingVoidType": "warn",
"noControlCharactersInRegex": "warn",
"noPrototypeBuiltins": "warn",
"noAssignInExpressions": "warn",
"noArrayIndexKey": "warn"
},
"a11y": {
"useSemanticElements": "warn",
"useKeyWithClickEvents": "warn",
"noSvgWithoutTitle": "warn",
"useButtonType": "warn",
"useIframeTitle": "warn",
"useAltText": "warn",
"noPositiveTabindex": "warn",
"useFocusableInteractive": "warn",
"useAriaPropsForRole": "warn",
"useValidAnchor": "warn",
"noLabelWithoutControl": "warn",
"noNoninteractiveTabindex": "warn"
},
"nursery": {
"useSortedClasses": {
"level": "error",
"fix": "safe",
"options": {
"attributes": ["class", "className", "style"],
"functions": ["clsx", "tw"]
}
}
}
}
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"overrides": [
{
"include": [
"packages/gitbook/**/*",
"packages/react-openapi/**/*",
"packages/react-math/**/*",
"packages/react-contentkit/**/*",
"packages/icons/**/*"
],
"javascript": {
"globals": ["React"]
}
},
{
"include": ["packages/gitbook/**/*"],
"javascript": {
"globals": ["React", "GitBookIntegrationEvent"]
}
},
{
"include": ["*.css"],
"javascript": {
"globals": ["theme"]
}
},
{
"include": ["*.test.ts", "packages/gitbook/tests/**/*"],
"javascript": {
"globals": ["Bun"]
}
},
{
"include": [
"packages/cache-do/**/*",
"packages/gitbook/cf-env.d.ts",
"packages/gitbook/src/cloudflare-entrypoint.ts"
],
"javascript": {
"globals": [
"DurableObjectLocationHint",
"DurableObjectNamespace",
"DurableObjectStub",
"ContinentCode",
"Fetcher",
"ExportedHandler"
]
}
}
]
}
+3260 -2299
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[install.scopes]
"gitbook" = { token = "$NPM_TOKEN_READONLY", url = "https://registry.npmjs.org" }
+22
View File
@@ -0,0 +1,22 @@
# Caching
## Revalidating the cache
Invalidate cache can be done at two levels using tags:
- Data fetching cache
- Rendering cache
To invalidate and refetch the data cache, you can execute a POST request to `/~/gitbook/revalidate`:
```bash
curl --location --request POST 'https://gitbook/mycompany.com/~gitbook/revalidate' \
--header 'Content-Type: application/json' \
--data-raw '{"tags": ["space.id"]}'
```
To invalidate the rendering cache, the implementation mainly depends on the infrastructure serving the content, GitBook outputs a `Cache-Tag` header on every requests. The value of the header is a comma separated list of tags.
## Purging the cache
Purging the cache, without revalidating, is done by passing `"purge": true` in the request body.
+20 -43
View File
@@ -1,67 +1,44 @@
{
"name": "gitbook",
"version": "0.1.0",
"engines": {
"node": "^22.3.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.31.0",
"turbo": "^2.10.3",
"vercel": "50.37.3"
"@changesets/cli": "^2.27.12",
"prettier": "^3.0.3",
"turbo": "^2.4.1-canary.2",
"vercel": "^39.3.0"
},
"packageManager": "bun@1.3.7",
"packageManager": "bun@1.1.18",
"overrides": {
"@codemirror/state": "6.4.1",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"esbuild": "0.27.3",
"axios": "1.8.4",
"@radix-ui/react-slot": "1.2.4"
"react": "18.3.1",
"react-dom": "18.3.1"
},
"private": true,
"scripts": {
"dev": "turbo run dev --concurrency 20",
"dev": "turbo run dev",
"dev:v2": "turbo run dev:v2",
"build": "turbo run build",
"build:v2": "turbo run build:v2",
"clean-deps": "rm -rf node_modules && rm -rf packages/*/node_modules",
"lint": "turbo run lint",
"lint:fix": "turbo run lint -- --fix",
"typecheck": "turbo run typecheck",
"format": "biome check --write ./",
"format:check": "biome check --diagnostic-level=error ./",
"format": "prettier ./ --ignore-unknown --write",
"format:check": "prettier ./ --ignore-unknown --list-different",
"unit": "turbo run unit",
"e2e": "turbo run e2e",
"e2e-customers": "turbo run e2e-customers",
"changeset": "changeset",
"changeset-version": "changeset version && bun run format && bun update",
"publish-all-packages": "turbo run publish-to-npm --continue=dependencies-successful",
"release": "turbo run release && changeset publish",
"release:preview": "turbo run release:preview",
"download:env": "op read op://gitbook-x-dev/gitbook-open/.env.local >> .env.local",
"clean": "turbo run clean"
},
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.189.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"assert-never": "^1.4.0",
"bidc": "^0.0.2",
"bun-types": "^1.1.20",
"classnames": "^2.5.1",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"tsdown": "^0.15.6",
"typescript": "^5.5.3",
"usehooks-ts": "^3.1.1",
"vercel": "^50.26.1"
}
},
"workspaces": [
"packages/*"
],
"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",
"next@16.2.6": "patches/next@16.2.6.patch"
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch"
}
}
-52
View File
@@ -1,52 +0,0 @@
# @gitbook/browser-types
## 0.1.5
### Patch Changes
- 5f66860: Use isCookiesTrackingDisabled for cookie consent integrations
- 2e495cb: Add Global Privacy Control (GPC) support
## 0.1.4
### Patch Changes
- 7a11861: Add support for custom cookie banner registration
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.1.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.1
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.0
### Minor Changes
- cbc71a5: First version of the public package for typing script integrations.
### Patch Changes
- 854c448: Custom assistants followup
- Updated dependencies [25e2b40]
- @gitbook/icons@0.3.0
-3
View File
@@ -1,3 +0,0 @@
# `@gitbook/browser-types`
Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.
-36
View File
@@ -1,36 +0,0 @@
{
"name": "@gitbook/browser-types",
"description": "Typescript types for the global variables available in a GitBook website. These types can be used by integrations embedding scripts.",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.1.5",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:"
},
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-108
View File
@@ -1,108 +0,0 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
export type GitBookIntegrationEvent = 'load' | 'unload';
export type GitBookIntegrationEventCallback = (...args: any[]) => void;
export type GitBookIntegrationTool = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
*/
confirmation?: {
icon?: IconName;
label: string;
};
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
export type GitBookAssistant = {
/**
* Name of the assistant displayed in the UI.
*/
label: string;
/**
* Icon of the assistant displayed in the UI.
* Any FontAwesome icon name is supported.
* @example 'sparkle'
*/
icon: string;
/**
* Callback when the assistant is opened.
*/
open: (query?: string) => void;
/**
* Whether to display the triggers for this assistant in the UI.
* @default true
*/
ui?: boolean;
};
export type GitBookCookieBannerHandler = (options: {
onApprove: () => void;
onReject: () => void;
}) => void;
export type GitBookGlobal = {
/**
* Register an event listener.
*/
addEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Remove an event listener.
*/
removeEventListener: (
type: GitBookIntegrationEvent,
func: GitBookIntegrationEventCallback
) => void;
/**
* Register a custom tool to be exposed to the AI assistant.
*/
registerTool: (tool: GitBookIntegrationTool) => void;
/**
* Register a custom assistant to be available on the site.
*/
registerAssistant: (assistant: GitBookAssistant) => () => void;
/**
* Register a custom cookie banner handler.
* When registered, the built-in cookie banner will not be displayed.
*/
registerCookieBanner: (handler: GitBookCookieBannerHandler) => void;
/**
* Indicates whether cookies tracking is disabled for the current user.
* Returns `true` when tracking is disabled, `false` when tracking is enabled,
* and `undefined` when the tracking preference is unknown or not yet determined.
*/
isCookiesTrackingDisabled: () => boolean | undefined;
/**
* Indicates whether global privacy control is enabled for the current user.
* Returns `true` when global privacy control is enabled, `false` when it is disabled.
*/
isGlobalPrivacyControlEnabled: () => boolean;
};
declare global {
interface Window {
/**
* Global `window.GitBook` object accessible by integrations.
*/
GitBook?: GitBookGlobal;
}
}
+3
View File
@@ -0,0 +1,3 @@
.wrangler
worker-configuration.d.ts
dist/
+18
View File
@@ -0,0 +1,18 @@
# @gitbook/cache-do
## 0.1.1
### Patch Changes
- b7a5106: Disable cloudflare observability in production
## 0.1.0
### Minor Changes
- 9b8d519: Experiment with optimizing billable duration in Cloudflare by using multiple RPC sessions instead of one
- 636b868: First version of a new cache backend powered by Cloudflare Durable Objects
### Patch Changes
- 56f5fa1: Enable Workers observability with a sampling of 0.1
+22
View File
@@ -0,0 +1,22 @@
# `@gitbook/cache-do`
Cache backend, powered by Cloudflare Durable Objects. The cache is optimized for GitBook use-cases.
### Performances
The cache backend is optimized for performances by being distributed and accessible close to the worker locations that are reading it.
### Geo-distribution
To achieve a good balance between **performances** and **consistency**, cache objects are distributed over 7 locations, representing continents.
It makes it possible to purge all 7 locations in one go and achieve fast consistency.
### Concepts
**Cache tag**: unique tag in the cache environment. A cache tag groups multiple keys that should be purged together in one operation.
Cache tags should not contain a large set of unique keys. Exceeding thousands could lead to performances or reliability issues.
**Cache key**: unique key in the cache environment. Each key should be assigned to a `tag`.
**Location**: cache is distributed over 7 unique locations, one for each continent.
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@gitbook/cache-do",
"type": "module",
"private": true,
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
},
"./api": {
"types": "./dist/api.d.ts",
"development": "./src/api.ts",
"default": "./dist/api.js"
}
},
"version": "0.1.1",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"lru_map": "^0.4.1"
},
"devDependencies": {
"typescript": "^5.5.3",
"wrangler": "3.82.0"
},
"scripts": {
"generate": "wrangler types --experimental-include-runtime",
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w",
"release": "wrangler deploy",
"release:preview": "wrangler deploy && wrangler deploy --env preview"
},
"files": [
"dist",
"src",
"bin",
"data",
"README.md",
"CHANGELOG.md"
]
}
+299
View File
@@ -0,0 +1,299 @@
import { encode, decode } from '@msgpack/msgpack';
import { DurableObject } from 'cloudflare:workers';
import { LRUMap } from 'lru_map';
export interface CacheObjectDescriptor {
get: <Value = unknown>(key: string) => Promise<Value | undefined>;
set: <Value = unknown>(key: string, value: Value, expiresAt: number) => Promise<void>;
}
/**
* Value stored in a chunked binary msgpack format.
* Stored under the key `prop.${key}.${index}`.
*/
interface CacheObjectProp<Value = unknown> {
value: Value;
expiresAt: number;
}
/**
* Expiration clock stored under the key `exp.${expiresAt}.${key}`.
*/
interface CacheObjectExp {
/** Key of the property */
k: string;
/** Number of chunks */
c: number;
}
/**
* Durable Object class being deployed as a distributed cache.
*/
export class CacheObject extends DurableObject {
private lru = new LRUMap<string, { match: CacheObjectProp | undefined }>(500);
/**
* Open a descriptor to access the cache object.
* The goal is to minimize the amount of RPC sessions between the client and the cache object.
* One session is opened per request on the client side and used to perform multiple operations.
* https://developers.cloudflare.com/workers/runtime-apis/rpc/#return-functions-from-rpc-methods
*/
public open(): CacheObjectDescriptor {
return {
get: async <Value = unknown>(key: string) => {
return this.get<Value>(key);
},
set: async <Value = unknown>(key: string, value: Value, expiresAt: number) => {
await this.set(key, value, expiresAt);
},
};
}
/**
* Get the value of a property.
*/
public async get<Value = unknown>(key: string) {
return this.logOperation({ operation: 'get', key }, async (setLog) => {
// Try the memory state first.
const memoryEntry = this.lru.get(key);
if (memoryEntry) {
setLog({ memory: true });
setLog({ memoryMatch: !!memoryEntry.match });
if (!memoryEntry.match) {
return;
}
const isExpired = memoryEntry.match.expiresAt < Date.now();
setLog({ memoryExpired: isExpired });
if (!isExpired) {
return memoryEntry.match.value as Value;
}
}
return await this.getFromStorage<Value>(key);
});
}
/**
* Get the value of a property from the DO storage.
*/
public async getFromStorage<Value = unknown>(key: string) {
return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
const entries = await this.ctx.storage.list<Uint8Array>({
prefix: getStoragePropKey(key),
noCache: true,
});
if (entries.size) {
const entry = decodeChunks<CacheObjectProp<Value>>(entries);
setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
if (entry && entry.value.expiresAt > Date.now()) {
// Found
this.lru.set(key, { match: entry.value });
return entry.value.value;
}
}
// Not found
this.lru.set(key, { match: undefined });
});
}
/**
* Set a value in the cache object.
*/
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
return this.logOperation({ operation: 'set', key }, async (setLog) => {
const prop: CacheObjectProp<Value> = {
value,
expiresAt,
};
this.lru.set(key, { match: prop });
await this.ctx.storage.transaction(async (tx) => {
const entries = encodeChunks(key, prop);
const chunks = Object.keys(entries).length;
setLog({ chunks });
const clockValue: CacheObjectExp = {
k: key,
c: chunks,
};
await tx.put(getGCClockKey(key, expiresAt), clockValue);
await tx.put(entries);
const currentAlarm = await tx.getAlarm();
if (!currentAlarm) {
// Set an alarm to garbage collect all entries that have expired in 12h.
await tx.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
}
});
});
}
/**
* Purge all keys in the cache object.
*/
public async purge() {
return this.logOperation({ operation: 'purge' }, async (setLog) => {
let result = new Set<string>();
try {
// List all the keys in the cache object.
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
entries.forEach((exp) => {
result.add(exp.k);
});
} catch (error) {
// If an error occurs, reset the cache object.
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
console.error('Error during purge, resetting the cache object', error);
}
await this.reset();
return Array.from(result);
});
}
/**
* Alarm to garbage collect all entries that have expired.
*/
async alarm() {
return this.logOperation({ operation: 'alarm' }, async (setLog) => {
try {
const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.',
noCache: true,
});
setLog({ entries: entries.size });
const toDeleteSet = new Set<string>();
for (const [key, exp] of entries) {
const timestamp = parseInt(key.split('.')[1]);
if (timestamp < Date.now()) {
toDeleteSet.add(key);
for (let i = 0; i < exp.c; i++) {
toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
}
}
}
// Delete the keys by batch of 128.
const toDelete = Array.from(toDeleteSet);
setLog({ toDelete: toDelete.length });
for (let i = 0; i < toDelete.length; i += 128) {
await this.ctx.storage.delete(toDelete.slice(i, i + 128));
}
// If there are still keys to delete, set an alarm to continue the deletion in 12h.
if (toDelete.length) {
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
}
} catch (error) {
// If an error occurs, reset the cache object.
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
console.error('Error during alarm, reset the cache object', error);
await this.reset();
}
});
}
/**
* Reset the cache object.
*/
async reset() {
return this.logOperation({ operation: 'reset' }, async () => {
this.lru.clear();
await this.ctx.storage.deleteAll();
});
}
/**
* Time and log an operation.
*/
async logOperation<T>(
log: Record<string, unknown>,
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>,
): Promise<T> {
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
let update: Record<string, unknown> = {};
const start = performance.now();
try {
return await fn((arg) => {
Object.assign(update, arg);
});
} finally {
const duration = performance.now() - start;
console.log({ ...log, ...update, objectId, duration });
}
}
}
function getStoragePropKey(key: string): string {
return `prop.${key}.`;
}
function getStoragePropChunkKey(key: string, index: number): string {
return `${getStoragePropKey(key)}${index}`;
}
function getGCClockRootKey(timestamp: number): string {
return `exp.${timestamp}.`;
}
function getGCClockKey(key: string, expiresAt: number): string {
return `${getGCClockRootKey(expiresAt)}${key}`;
}
function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
const buf = encode(value);
const entries: Record<string, Uint8Array> = {};
const chunks = chunkUint8Array(buf, 128 * 1024);
for (let index = 0; index < chunks.length; index++) {
entries[getStoragePropChunkKey(key, index)] = chunks[index];
}
return entries;
}
function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
const chunks = Array.from(entries.entries())
.map(([key, value]) => {
const index = parseInt(key.split('.').pop()!);
return [index, value] as const;
})
.sort(([a], [b]) => a - b)
.map(([, value]) => value);
if (chunks.length === 0) {
return;
}
const buf = mergeUint8Array(chunks);
return { value: decode(buf) as T, size: buf.length };
}
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
const chunks: Uint8Array[] = [];
for (let i = 0; i < input.length; i += chunkSize) {
chunks.push(input.slice(i, i + chunkSize));
}
return chunks;
}
function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
+97
View File
@@ -0,0 +1,97 @@
import type { CacheObject, CacheObjectDescriptor } from './CacheObject';
export type CacheLocationId = ContinentCode;
const allLocations: CacheLocationId[] = ['AF', 'AS', 'NA', 'SA', 'AN', 'EU', 'OC'];
/**
* Location hint for the CacheObject durable object.
*/
const doLocationHints: {
[key in CacheLocationId]: DurableObjectLocationHint;
} = {
AF: 'afr',
AS: 'apac',
NA: 'wnam',
SA: 'sam',
AN: 'oc',
EU: 'weur',
OC: 'oc',
};
/**
* Client to access a cache tag.
*/
export class CacheObjectStub {
private stub: DurableObjectStub<CacheObject>;
constructor(
/** Binding to the CacheObject durable object */
private doNamespace: DurableObjectNamespace<CacheObject>,
/** ID of the location to target */
private locationId: CacheLocationId,
/** Name of the tag */
private tag: string,
) {
const groupId = getCacheObjectIdName(this.locationId, this.tag);
this.stub = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
// Initialize the object with a locaiton hint,
// as we might want to purge all locations before the object is created.
// https://developers.cloudflare.com/durable-objects/reference/data-location/
locationHint: doLocationHints[this.locationId],
});
}
/**
* Open a descriptor to the cache object.
* It can be used to perform multiple operations in a single RPC session.
* Ex:
* ```ts
* using desc = cache.open();
* await desc.set('key', 'value', Date.now() + 1000);
* await desc.get('key');
* ```
*/
async open() {
return await this.stub.open();
}
/**
* Get a value from the cache.
*/
async get<Value = unknown>(key: string) {
return (await this.stub.get(key)) as Value | undefined;
}
/**
* Set a value in the cache.
*/
async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
return await this.stub.set(key, value, expiresAt);
}
/**
* Purge all keys in the cache tag.
*/
async purge() {
const keys = new Set<string>();
await Promise.all(
allLocations.map(async (locationId) => {
const groupId = getCacheObjectIdName(locationId, this.tag);
const cacheGroup = this.doNamespace.get(this.doNamespace.idFromName(groupId), {
// Initialize the object with a locaiton hint,
// as we might want to purge all locations before the object is created.
// https://developers.cloudflare.com/durable-objects/reference/data-location/
locationHint: doLocationHints[this.locationId],
});
const locationkeys = await cacheGroup.purge();
locationkeys.forEach((key) => keys.add(key));
}),
);
return keys;
}
}
function getCacheObjectIdName(locationId: CacheLocationId, tag: string): string {
return `${locationId}:${tag}`;
}
+1
View File
@@ -0,0 +1 @@
export * from './CacheObjectStub';
+9
View File
@@ -0,0 +1,9 @@
import { WorkerEntrypoint } from 'cloudflare:workers';
export * from './CacheObject';
export default class Worker extends WorkerEntrypoint {
fetch() {
return new Response('Hello, world!');
}
}
@@ -5,7 +5,6 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
@@ -14,12 +13,9 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
"types": ["./.wrangler/types/runtime.d.ts"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
+27
View File
@@ -0,0 +1,27 @@
main = "./src/index.ts"
name = "gitbook-open-cache"
compatibility_date = "2024-09-02"
durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"}
]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[observability]
enabled = false
[env.preview]
name = "gitbook-open-cache-preview"
durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"}
]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[env.preview.observability]
enabled = true
head_sampling_rate = 0.1
-1
View File
@@ -1 +0,0 @@
dist/
-43
View File
@@ -1,43 +0,0 @@
# @gitbook/cache-tags
## 0.3.4
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.3.3
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.3.2
### Patch Changes
- 295f03d: Republish packages
## 0.3.1
### Patch Changes
- 77397ca: Fix version of @gitbook/api referenced in package.json
## 0.3.0
### Minor Changes
- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification
## 0.2.0
### Minor Changes
- f32bf1f: Export function `getCacheTagForURL` to easily get the cache tag for a URL.
## 0.1.0
### Minor Changes
- 05ffd0e: Initial version of the package
-3
View File
@@ -1,3 +0,0 @@
# `@gitbook/cache-tags`
Utility to generate cache tags for GitBook Open.
-35
View File
@@ -1,35 +0,0 @@
{
"name": "@gitbook/cache-tags",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.3.4",
"dependencies": {
"@gitbook/api": "catalog:",
"assert-never": "catalog:"
},
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
-201
View File
@@ -1,201 +0,0 @@
import type { ComputedContentSource } from '@gitbook/api';
import assertNever from 'assert-never';
/**
* Get a stringified cache tag for a given object.
*/
export function getCacheTag(
spec: /**
* All data related to a user
* @deprecated - in v2, no tag as this is an immutable data
*/
| {
tag: 'user';
user: string;
}
/**
* All data related to a space
*/
| {
tag: 'space';
space: string;
}
/**
* All data related to an integration.
*/
| {
tag: 'integration';
integration: string;
}
/**
* All data related to a change request
*/
| {
tag: 'change-request';
space: string;
changeRequest: string;
}
/**
* Immutable data related to a revision
* @deprecated - in v2, no tag as this is an immutable data
*/
| {
tag: 'revision';
space: string;
revision: string;
}
/**
* Immutable data related to a document
* @deprecated - in v2, no tag as this is an immutable data
*/
| {
tag: 'document';
space: string;
document: string;
}
/**
* Immutable data related to a computed document
* @deprecated - in v2, no tag as this is an immutable data
*/
| {
tag: 'computed-document';
space: string;
sourceType: string;
}
/**
* All data related to the URL of a content
*/
| {
tag: 'url';
hostname: string;
}
/**
* All data related to a site
*/
| {
tag: 'site';
site: string;
}
/**
* All data related to an OpenAPI spec
*/
| {
tag: 'openapi';
organization: string;
openAPISpec: string;
}
/**
* All data related to a translation
*/
| {
tag: 'translation';
organization: string;
translation: string;
}
): string {
switch (spec.tag) {
case 'user':
return `user:${spec.user}`;
case 'url':
return `url:${spec.hostname}`;
case 'space':
return `space:${spec.space}`;
case 'change-request':
return `space:${spec.space}:change-request:${spec.changeRequest}`;
case 'revision':
return `space:${spec.space}:revision:${spec.revision}`;
case 'document':
return `space:${spec.space}:document:${spec.document}`;
case 'computed-document':
return `space:${spec.space}:computed-document:${spec.sourceType}`;
case 'site':
return `site:${spec.site}`;
case 'integration':
return `integration:${spec.integration}`;
case 'openapi':
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
case 'translation':
return `organization:${spec.organization}:translation:${spec.translation}`;
default:
assertNever(spec);
}
}
/**
* Get the tags for a computed content source.
*/
export function getComputedContentSourceCacheTags(
inContext: {
spaceId: string;
organizationId: string;
},
source: ComputedContentSource
) {
const tags: string[] = [];
if (!('dependencies' in source)) {
return tags;
}
// We add the dependencies as tags, to ensure that the computed content is invalidated
// when the dependencies are updated.
const dependencies = Object.values(source.dependencies ?? {});
if (dependencies.length > 0) {
dependencies.forEach((dependency) => {
switch (dependency.ref.kind) {
case 'space':
tags.push(
getCacheTag({
tag: 'space',
space: dependency.ref.space,
})
);
break;
case 'openapi':
tags.push(
getCacheTag({
tag: 'openapi',
organization: inContext.organizationId,
openAPISpec: dependency.ref.spec,
})
);
break;
case 'translation':
tags.push(
getCacheTag({
tag: 'translation',
organization: inContext.organizationId,
translation: dependency.ref.translation,
})
);
break;
default:
// Do not throw for unknown dependency types
// as it might mean we are lagging behind the API version
break;
}
});
} else {
// Push a dummy tag, as the v1 is only using the first tag
tags.push(
getCacheTag({
tag: 'computed-document',
space: inContext.spaceId,
sourceType: source.type,
})
);
}
// We invalidate the computed content when a new version of the integration is deployed.
if (source.type.startsWith('integration:')) {
const integration = source.type.split(':')[1]!;
tags.push(
getCacheTag({
tag: 'integration',
integration,
})
);
}
return tags;
}
+1 -55
View File
@@ -1,61 +1,7 @@
# @gitbook/colors
## 0.4.3
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
## 0.4.2
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
## 0.4.1
### Patch Changes
- 295f03d: Republish packages
## 0.4.0
### Minor Changes
- 17dd382: Add `original` background color step
### Patch Changes
- 193d591: Fix return type for `colorContrast`
## 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
- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale
## 0.3.1
### Patch Changes
- fb90eb0: Reduce chroma of first color scale step
## 0.3.0
### Minor Changes
- 4f0a772: Override tint lightness if supplied color is out of bounds
## 0.2.0
### Minor Changes
- 445baaa: Initial release
- 445baaa: Initial release
+11 -17
View File
@@ -4,29 +4,23 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"sideEffects": false,
"version": "0.4.3",
"version": "0.2.0",
"devDependencies": {
"bun-types": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:"
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsdown",
"build": "tsc",
"typecheck": "tsc --noEmit",
"unit": "bun test",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
"dev": "tsc -w"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
"files": [
"dist",
"src",
"README.md",
"CHANGELOG.md"
]
}
@@ -1,69 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { colorScale } from './transformations';
describe('colorScale exact base', () => {
it('takes a very light tint as the exact background on step 1 (clean/bold)', () => {
const scale = colorScale('#F5F3EF', { baseStep: 1 });
expect(scale[0]).toBe('#F5F3EF');
});
it('anchors a very light tint to step 2 when the theme uses the subtle step (muted)', () => {
const scale = colorScale('#F5F3EF', { baseStep: 2 });
expect(scale[1]).toBe('#F5F3EF');
// Step 1 sits just above the exact base, toward white.
expect(scale[0]).not.toBe('#F5F3EF');
expect(scale[0]).not.toBe('#ffffff');
});
it('takes a darker-than-dark tint as the exact background, preserving hue and chroma', () => {
const scale = colorScale('#0B0F19', { darkMode: true, baseStep: 1 });
expect(scale[0]).toBe('#0B0F19');
});
it('does not trigger for a normal mid-lightness tint', () => {
const scale = colorScale('#787878', { baseStep: 2 });
// The default white background is kept; the tint only colors the scale.
expect(scale[0]).toBe('#ffffff');
expect(scale[1]).not.toBe('#787878');
});
it('does not trigger for a saturated light color, keeping the normal accent ramp', () => {
// Light enough (L≈0.93) to pass the lightness bound, but too chromatic to read as a
// background — emitting it verbatim would leave a vivid step 1 above a near-gray scale.
const scale = colorScale('#FFEB3B', { baseStep: 1 });
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#FFEB3B');
});
it('respects a custom light background instead of overriding it with the tint', () => {
// The color is darker than the requested background, so it is not the extreme end and the
// supplied base must be preserved rather than overwritten.
const scale = colorScale('#eeeeee', { baseStep: 1, background: '#f8f8f8' });
expect(scale[0]).not.toBe('#eeeeee');
});
it('anchors an exact base even when a neutral mix is supplied (tint === primary)', () => {
// getTintMixColor blends neutral into the tint when it equals the primary color; that must
// not darken a near-white tint out of the exact-base path.
const scale = colorScale('#F5F3EF', {
baseStep: 1,
mix: { color: '#787878', ratio: 0.4 },
});
expect(scale[0]).toBe('#F5F3EF');
});
it('does not trigger for a light accent color below the near-white threshold', () => {
// #D8DEEC (light blue-gray, L≈0.90) is a UI accent, not a background, so it must not anchor.
const scale = colorScale('#D8DEEC', { baseStep: 1 });
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#D8DEEC');
});
it('never anchors when no baseStep is given (accent scales and the bold theme)', () => {
// A scale that does not define the page background opts out of the exact base entirely.
const scale = colorScale('#F5F3EF', {});
expect(scale[0]).toBe('#ffffff');
expect(scale[0]).not.toBe('#F5F3EF');
});
});
+18 -103
View File
@@ -1,4 +1,4 @@
import { DARK_BASE, DEFAULT_TINT_COLOR, LIGHT_BASE } from './colors';
import { DARK_BASE, LIGHT_BASE, DEFAULT_TINT_COLOR } from './colors';
type ColorShades = {
[key: string]: string;
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
type OKLABColor = { L: number; A: number; B: number };
type OKLCHColor = { L: number; C: number; H: number };
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
export enum ColorCategory {
backgrounds = 'backgrounds',
@@ -19,7 +19,7 @@ export enum ColorCategory {
}
type ColorSubScale = {
[key: string]: number | string;
[key: string]: number;
};
/**
@@ -56,8 +56,6 @@ export const scale: Record<ColorCategory, ColorSubScale> = {
solid: 9,
/** Hovered solid backgrounds */
'solid-hover': 10,
/** Original color */
original: 'original',
},
[ColorCategory.text]: {
/** Very low-contrast text
@@ -82,22 +80,6 @@ export const colorMixMapping = {
dark: [0, 0.03, 0.08, 0.1, 0.13, 0.15, 0.2, 0.25, 0.5, 0.55, 0.75, 1],
};
/**
* Light mode has no equivalent to the dark base bound (nothing is lighter than white), so a tint
* at or above this lightness is treated as an explicit, near-white background (e.g. a warm `#F5F3EF`
* at L≈0.96). Kept high so light UI accent colors (around L≈0.90) aren't mistaken for backgrounds.
*/
const EXACT_BASE_LIGHT_THRESHOLD = 0.95;
/**
* Only a near-neutral tint reads as a background. A saturated color would keep the exact hue at the
* anchored step while the rest of the low scale stays ~gray, so those keep the normal accent ramp.
*/
const EXACT_BASE_NEUTRAL_CHROMA = 0.05;
/** Lightness of the default white light background (≈0.99999, not exactly 1). */
const LIGHT_BASE_L = rgbToOklch(hexToRgbArray(LIGHT_BASE)).L;
/**
* Convert a hex color to an RGB color.
*/
@@ -175,14 +157,6 @@ export type ColorScaleOptions = {
/** Define a custom foreground color to use. If left undefined, the global `light`/`dark` values (in `colors.ts`) will be used. */
foreground?: string;
/**
* The 1-indexed scale step this scale renders as the page background (1 = `tint-base` for
* `clean`, 2 = `tint-subtle` for `muted`). When set, an extreme near-neutral tint is taken as
* the exact background, anchored to this step so it matches exactly. Omit for scales that don't
* define the page background (accents, or the two-tone `bold` theme) — they never anchor.
*/
baseStep?: number;
mix?: {
/** If set to a hex code, this color will be additionally mixed into the generated scale according to `mix.ratio`. */
color: string;
@@ -203,92 +177,36 @@ export function colorScale(
darkMode = false,
background = darkMode ? DARK_BASE : LIGHT_BASE,
foreground = darkMode ? LIGHT_BASE : DARK_BASE,
baseStep,
mix,
}: ColorScaleOptions = {}
}: ColorScaleOptions = {},
) {
const baseColor = rgbToOklch(hexToRgbArray(hex));
const mixColor = mix?.color ? rgbToOklch(hexToRgbArray(mix.color)) : null;
const foregroundColor = rgbToOklch(hexToRgbArray(foreground));
const backgroundColor = rgbToOklch(hexToRgbArray(background));
let mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
// A near-neutral tint at the extreme end of the scale is taken as the exact page background
// rather than tinting pure black/white with it — letting brands set an exact background such as
// a warm `#F5F3EF`. Only scales that define the page background opt in (via `baseStep`). In light
// mode the base is pure white by default, so a near-white tint also qualifies (nothing is lighter
// than white); a custom, lower background is respected instead. Decided on the raw color so a
// neutral mix (below) can't darken a tint out of the exact base.
const isExtremeBase = darkMode
? baseColor.L < backgroundColor.L
: backgroundColor.L >= LIGHT_BASE_L
? baseColor.L > EXACT_BASE_LIGHT_THRESHOLD
: baseColor.L > backgroundColor.L;
const isExactBase =
baseStep !== undefined && isExtremeBase && baseColor.C < EXACT_BASE_NEUTRAL_CHROMA;
const exactBaseIndex = (baseStep ?? 1) - 1;
if (mixColor && mix?.ratio && mix.ratio > 0 && !isExactBase) {
// Mix a little of the mix color into the base — but not when the tint is the exact base,
// where it must stay true to the supplied color (and match `--header-background`).
if (mixColor && mix?.ratio && mix.ratio > 0) {
// If defined, we mix in a (tiny) bit of the mix color with the base color.
baseColor.L = mixColor.L * mix.ratio + baseColor.L * (1 - mix.ratio);
baseColor.C = mixColor.C * mix.ratio + baseColor.C * (1 - mix.ratio);
baseColor.H = mix.color === DEFAULT_TINT_COLOR ? baseColor.H : mixColor.H;
}
if (isExactBase) {
const difference = (backgroundColor.L - baseColor.L) / backgroundColor.L;
// At the edges of the scale, the subtle lightness changes stop being perceptible. We need to amp up our mapping to still stand out.
const amplifier = 1;
mapping = mapping.map((step, index) =>
index < 9 ? step + step * amplifier * difference : step
);
// Anchor the supplied color to the step the theme renders as the background, solving the
// background lightness so neighbouring steps stay continuous with it.
const baseMix = mapping[exactBaseIndex]!;
backgroundColor.L = (baseColor.L - foregroundColor.L * baseMix) / (1 - baseMix);
}
const mapping = darkMode ? colorMixMapping.dark : colorMixMapping.light;
const result = [];
for (let index = 0; index < mapping.length; index++) {
if (isExactBase && index === exactBaseIndex) {
result.push(hex);
continue;
}
const targetL =
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
const step = mapping[index]!;
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
if (
index === 8 &&
!mix &&
(darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2)
) {
if (index === 8 && !mix && Math.abs(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;
}
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:
// When the tint is the exact base, hold the steps from the base toward the
// accents at its chroma so the background stays tinted; steps lighter than the
// base (e.g. cards in `muted`) keep desaturating toward white.
return isExactBase && index >= exactBaseIndex ? 1 : index * 0.05;
}
})();
const chromaRatio = index < 8 ? (index + 1) * 0.05 : 1;
const shade = {
L: targetL, // Blend lightness
@@ -345,7 +263,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
const rgb = end.map((channel, index) => {
return Math.round(channel + percentage * (start[index]! - channel));
return Math.round(channel + percentage * (start[index] - channel));
});
return rgbArrayToHex(rgb as RGBColor);
@@ -442,14 +360,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
B: number;
} {
const [x, y, z] = xyz.map((v, i) => {
const scaled = v / D65[i]!;
const scaled = v / D65[i];
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
});
return {
L: 116 * y! - 16,
A: 500 * (x! - y!),
B: 200 * (y! - z!),
L: 116 * y - 16,
A: 500 * (x - y),
B: 200 * (y - z),
};
}
@@ -469,10 +387,7 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
return contrast < 7.5 ? 0 : contrast;
}
export function colorContrast(
background: string,
foreground: string[] = [LIGHT_BASE, DARK_BASE]
): string {
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
const bg = hexToRgbArray(background);
const best: { color?: RGBColor; contrast: number } = {
@@ -489,5 +404,5 @@ export function colorContrast(
}
}
return best.color ? rgbArrayToHex(best.color) : foreground[0] || LIGHT_BASE;
return best.color ? rgbArrayToHex(best.color) : foreground[0];
}
+1 -2
View File
@@ -5,7 +5,6 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
@@ -14,7 +13,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "react",
"incremental": true,
"types": [
"bun-types" // add Bun global
-2
View File
@@ -1,2 +0,0 @@
dist/
standalone/
-105
View File
@@ -1,105 +0,0 @@
# @gitbook/embed
## 0.5.1
### Patch Changes
- 37a9453: Allow copying in embed
- Updated dependencies [47c8ccb]
- @gitbook/icons@0.5.0
## 0.5.0
### Minor Changes
- 88c38fa: Add Search tab to Docs Embed, refactor search
### Patch Changes
- 8d2a95b: Docs Embed: Better support light/dark mode overrides
## 0.4.0
### Minor Changes
- b3875a1: Support `assistantName` property to override Assistant name
## 0.3.0
### Minor Changes
- 370b731: Docs Embed: Make trademark optional
## 0.2.2
### Patch Changes
- b4b471f: Fix unexpected error thrown when using the embed in standalone mode
## 0.2.1
### Patch Changes
- 96e24a1: Fix signed JWT token usage
- 0aa34cc: Fix Embed options in React
- Updated dependencies [6f1db32]
- @gitbook/icons@0.4.3
## 0.2.0
### Minor Changes
- 1430cee: Improve Docs Embed with separate Assistant and Docs tabs
### Patch Changes
- Updated dependencies [62438f4]
- @gitbook/icons@0.4.0
## 0.1.5
### Patch Changes
- 10995e0: Use NPM Trusted publishing for publishing the package.
- Updated dependencies [10995e0]
- @gitbook/icons@0.3.4
## 0.1.4
### Patch Changes
- 6142d6b: Mark as sideEffects, fix all package bundles
- Updated dependencies [6142d6b]
- @gitbook/icons@0.3.3
## 0.1.3
### Patch Changes
- 295f03d: Republish packages
- Updated dependencies [295f03d]
- @gitbook/icons@0.3.2
## 0.1.2
### Patch Changes
- 7508674: Fix bundling of packages
## 0.1.1
### Patch Changes
- 6f368b5: Fix embed assistant window width on small screens
## 0.1.0
### Minor Changes
- 81a6bd7: Improve API to control the GitBook embed
### Patch Changes
- 8927e8f: Initial version of the embed SDK.
- Updated dependencies [25e2b40]
- @gitbook/icons@0.3.0
-497
View File
@@ -1,497 +0,0 @@
# GitBook Docs Embed (`@gitbook/embed`)
Embed your GitBook docs in your product or website.
The Docs Embed can contain three tabs:
- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers
- **Search**: A search-focused surface for quickly finding pages and asking scoped questions
- **Docs**: A browser for navigating your documentation site
The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options.
# Usage
## As a standalone script from your docs site
All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website.
The script is served at `https://docs.company.com/~gitbook/embed/script.js`.
You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname.
```html
<script src="https://docs.company.com/~gitbook/embed/script.js"></script>
<script>
// Initialize with Authenticated Access (optional)
window.GitBook('init',
{ siteURL: 'https://docs.company.com' },
{ visitor: { token: 'your-jwt-token' } }
);
window.GitBook('show');
</script>
```
The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods.
### Example: Configuring the widget
```javascript
GitBook('configure', {
button: {
label: 'Ask',
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
},
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true,
});
```
See the [Configuration](#configuration) section for all available options.
## As a package from NPM
Install the package: `npm install @gitbook/embed` and import it in your web application:
```tsx
import { createGitBook } from '@gitbook/embed';
const gitbook = createGitBook({
siteURL: 'https://docs.company.com'
});
// Create an iframe and get its URL
const iframe = document.createElement('iframe');
iframe.src = gitbook.getFrameURL({
colorScheme: 'dark', // Optional: force the embed to render in dark mode
visitor: {
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { // Optional: custom claims for dynamic expressions
userId: '123',
plan: 'premium'
}
}
});
// Create a frame client to communicate with the iframe
const frame = gitbook.createFrame(iframe);
// Use the frame client methods
frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab
frame.navigateToAssistant(); // Switch to the assistant tab
frame.postUserMessage('How do I get started?');
frame.clearChat();
// Configure the embed (see Configuration section for all options)
frame.configure({
tabs: ['assistant', 'search', 'docs'],
actions: [
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
],
greeting: { title: 'Welcome!', subtitle: 'How can I help?' },
assistantName: 'Support Assistant',
suggestions: ['What is GitBook?', 'How do I get started?'],
tools: [/* ... */],
closeButton: true
});
// Listen to events
frame.on('close', () => {
console.log('Frame closed');
});
```
## As React components
After installing the NPM package, you can import prebuilt React components:
```tsx
import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
<GitBookProvider siteURL="https://docs.company.com">
<GitBookFrame
colorScheme="dark"
visitor={{
token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
}}
tabs={['assistant', 'search', 'docs']}
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
assistantName="Support Assistant"
suggestions={['What is GitBook?', 'How do I get started?']}
actions={[
{
icon: 'circle-question',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
}
]}
tools={[/* ... */]}
closeButton
/>
</GitBookProvider>
```
You can also use the `useGitBook` hook to access the client:
```tsx
import { useGitBook } from '@gitbook/embed/react';
function MyComponent() {
const gitbook = useGitBook();
const frameURL = gitbook.getFrameURL({ colorScheme: 'dark', visitor: { token: '...' } });
// ...
}
```
# API Reference
## Method Comparison
| Method | Standalone Script | NPM Package | React Components |
|--------|------------------|-------------|------------------|
| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `<GitBookProvider siteURL="...">` |
| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` |
| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` |
| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ |
| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ |
| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client |
| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client |
| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client |
| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client |
| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `<GitBookFrame>` |
| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client |
| **Unload** | `GitBook('unload')` | ❌ | ❌ |
## Method Signatures
### Standalone Script
- `GitBook('init', options: { siteURL: string }, frameOptions?: { colorScheme?: 'light' | 'dark', visitor?: {...} })` - Initialize widget
- `GitBook('show')` - Show widget button
- `GitBook('hide')` - Hide widget button
- `GitBook('open')` - Open widget window
- `GitBook('close')` - Close widget window
- `GitBook('toggle')` - Toggle widget window
- `GitBook('navigateToPage', path: string)` - Navigate to page (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
- `GitBook('navigateToAssistant')` - Navigate to assistant tab
- `GitBook('postUserMessage', message: string)` - Post message to chat
- `GitBook('clearChat')` - Clear chat history
- `GitBook('configure', settings: {...})` - Configure widget
- `GitBook('unload')` - Unload widget
### NPM Package
**Client Factory:**
- `createGitBook(options: { siteURL: string })``GitBookClient`
- `client.getFrameURL(options?: { colorScheme?: 'light' | 'dark', visitor?: {...} })``string`
- `client.createFrame(iframe: HTMLIFrameElement)``GitBookFrameClient`
**Frame Client:**
- `frame.navigateToPage(path: string)``void` (accepts the page path within the site, an absolute path, or the full published URL; resolves pages in any space/section)
- `frame.navigateToAssistant()``void`
- `frame.postUserMessage(message: string)``void`
- `frame.clearChat()``void`
- `frame.configure(settings: Partial<GitBookEmbeddableConfiguration>)``void`
- `frame.on(event: string, listener: Function)``() => void` (unsubscribe)
### React Components
**Components:**
- `<GitBookProvider siteURL: string>` - Provider component
- `<GitBookFrame {...props}>` - Frame component (accepts all config options as props)
**Hooks:**
- `useGitBook()``GitBookClient` (must be used within `<GitBookProvider>`)
# Configuration
Configuration options are available across usage methods as follows:
- **Standalone script**: via `GitBook('configure', {...})`
- **NPM package**: via `frame.configure({...})`
- **React components**: via props on `<GitBookFrame>`
### `tabs`
Available in: Standalone script, NPM package, React components
Override which tabs are displayed. Defaults to your site's configuration.
- **Type**: `('assistant' | 'search' | 'docs')[]`
```javascript
tabs: ['assistant', 'search', 'docs']
```
### `closeButton`
Available in: Standalone script, NPM package, React components
Display a close (`x`) button in the embed sidebar.
- **Type**: `boolean`
- **Default**: `false`
```javascript
closeButton: true
```
Behavior:
- When clicked, the embed sends a `close` event to the parent page.
- In the standalone script, this event is handled automatically and the widget closes.
- In custom iframe integrations (NPM package), you must listen for the `close` event and decide how to hide/collapse your UI.
- In React, this works automatically when using the standard widget flow. If you build custom frame wiring, handle `close` the same way as the NPM package.
NPM package example:
```typescript
const frame = gitbook.createFrame(iframe);
frame.configure({
closeButton: true
});
const unsubscribe = frame.on('close', () => {
// Hide your modal/drawer/container
container.classList.add('hidden');
});
```
Notes:
- The close button is rendered in the same sidebar area as tabs/actions.
- If your UI hides that sidebar or doesn't render it, the button will not be visible.
### `trademark`
Available in: Standalone script, NPM package, React components
Display GitBook branding in the embed. Defaults to true.
- **Type**: `boolean`
- **Default**: `true`
```javascript
trademark: true
```
### `actions`
Available in: Standalone script, NPM package, React components
Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked.
**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality.
- **Type**: `GitBookEmbeddableActionDefinition[]`
- **Properties**:
- `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...)
- `label`: `string` - Button label text
- `onClick`: `() => void | Promise<void>` - Callback function when clicked
```javascript
actions: [
{
icon: 'comments',
label: 'Contact Support',
onClick: () => window.open('https://support.example.com', '_blank')
},
{
icon: 'rocket',
label: 'Get started',
onClick: () => {
GitBook('navigateToPage', '/getting-started');
}
}
]
```
### `greeting`
Available in: Standalone script, NPM package, React components
Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab.
- **Type**: `{ title: string, subtitle: string }`
```javascript
greeting: {
title: 'Welcome!',
subtitle: 'How can I help you today?'
}
```
### `assistantName`
Available in: Standalone script, NPM package, React components
Override the assistant name displayed in the chat header and assistant entry points (for example, sidebar tabs and action labels). This name will be limited to 32 characters to prevent text overflow.
- **Type**: `string`
```javascript
assistantName: 'Support Assistant'
```
### `suggestions`
Available in: Standalone script, NPM package, React components
Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen.
- **Type**: `string[]`
```javascript
suggestions: [
'What is GitBook?',
'How do I get started?',
'What can you do?'
]
```
### `tools`
Available in: Standalone script, NPM package, React components
Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems.
**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings.
- **Type**: `GitBookToolDefinition[]`
- **Properties**:
- `name`: `string` - Unique tool identifier
- `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it).
- `inputSchema`: `object` - JSON schema defining the tool's input parameters
- `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool.
- `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user.
- `summary`: The visual summary of the tool execution, shown in the user's chat window.
- `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval.
```javascript
tools: [
{
name: 'get_user_info',
description: 'Get information about the current user',
inputSchema: {
type: 'object',
properties: {
userId: {
type: 'string',
description: 'The user ID to look up'
}
},
required: ['userId']
},
execute: async (input) => {
const user = await fetch(`/api/users/${input.userId}`).then(r => r.json());
return {
output: { name: user.name, plan: user.plan },
summary: `Retrieved info for user ${user.name}`
};
}
},
{
name: 'create_ticket',
description: 'Create a support ticket',
confirmation: {
icon: 'circle-question',
label: 'Create support ticket?'
},
inputSchema: {
type: 'object',
properties: {
subject: { type: 'string' },
description: { type: 'string' }
},
required: ['subject', 'description']
},
execute: async (input) => {
const ticket = await fetch('/api/tickets', {
method: 'POST',
body: JSON.stringify(input)
}).then(r => r.json());
return {
output: { ticketId: ticket.id },
summary: `Created ticket #${ticket.id}`
};
}
}
]
```
### `visitor` (Authenticated Access)
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access).
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ visitor: {...} })`
**React components**: Pass as the `visitor` prop on `<GitBookFrame>`
- **Type**: `{ token?: string, unsignedClaims?: Record<string, unknown> }`
- **Properties**:
- `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access)
- `unsignedClaims`: `Record<string, unknown>` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.<claim-name>`
```javascript
visitor: {
token: 'your-jwt-token',
unsignedClaims: {
userId: '123',
plan: 'premium',
role: 'admin'
}
}
```
### `colorScheme`
Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop)
Override the embed's color scheme. When omitted, the embed follows the iframe's CSS `color-scheme`, which lets it inherit the parent page or browser preference.
**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL.
**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)`
**NPM package**: Pass to `getFrameURL({ colorScheme: 'dark' })`
**React components**: Pass as the `colorScheme` prop on `<GitBookFrame>`
- **Type**: `'light' | 'dark'`
```javascript
colorScheme: 'dark'
```
### `button`
Available in: Standalone script only
Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely.
- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }`
- **Properties**:
- `label`: `string` - Button label text
- `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets.
```javascript
button: {
label: 'Ask',
icon: 'assistant'
}
```
-50
View File
@@ -1,50 +0,0 @@
{
"name": "@gitbook/embed",
"description": "Embeddable components for GitBook",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./react": {
"types": "./dist/react/index.d.ts",
"default": "./dist/react/index.js"
}
},
"sideEffects": false,
"version": "0.5.1",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
"bidc": "catalog:"
},
"peerDependencies": {
"react": "*"
},
"devDependencies": {
"bun-types": "catalog:",
"@types/react": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"react": "catalog:"
},
"scripts": {
"build": "bun run build-lib && bun run build-standalone",
"build-lib": "tsdown",
"build-standalone": "bun build src/standalone/index.ts --bundle --minify --outdir=standalone",
"clean": "rm -rf ./dist",
"unit": "bun test",
"typecheck": "tsc --noEmit",
"dev": "bun run build -- --watch ./src",
"publish-to-npm": "../../scripts/publish-if-new.sh"
},
"files": ["dist", "README.md", "CHANGELOG.md"],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"url": "https://github.com/GitbookIO/gitbook"
}
}
@@ -1,51 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { createGitBook } from './createGitBook';
describe('createGitBook.getFrameURL', () => {
it('builds the embed URL when the site URL has no trailing slash', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs' });
const url = new URL(client.getFrameURL({}));
expect(url.origin).toBe('https://example.com');
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.toString()).toBe('');
});
it('handles a trailing slash and adds visitor parameters', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
visitor: {
token: 'signed-token',
unsignedClaims: {
role: 'editor',
count: 3,
enabled: false,
},
},
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('jwt_token')).toBe('signed-token');
expect(url.searchParams.get('visitor.role')).toBe('editor');
expect(url.searchParams.get('visitor.count')).toBe('3');
expect(url.searchParams.get('visitor.enabled')).toBe('false');
});
it('adds an explicit color scheme override when requested', () => {
const client = createGitBook({ siteURL: 'https://example.com/docs/' });
const url = new URL(
client.getFrameURL({
colorScheme: 'dark',
})
);
expect(url.pathname).toBe('/docs/~gitbook/embed');
expect(url.searchParams.get('theme')).toBe('dark');
});
});
@@ -1,71 +0,0 @@
import { type GitBookFrameClient, createGitBookFrame } from './createGitBookFrame';
export type CreateGitBookOptions = {
/**
* URL of the GitBook site to embed.
*/
siteURL: string;
};
export type GetFrameURLOptions = {
/**
* Override the color scheme used by the embedded docs.
* When omitted, the embed follows the iframe's CSS `color-scheme`.
*/
colorScheme?: 'light' | 'dark';
/**
* Authentication to use for the frame.
*/
visitor?: {
/**
* Signed JWT token for Adaptive Content or Visitor Authentication to use.
*/
token?: string;
/**
* Unsigned claims to pass to the frame.
* You can use these claims in dynamic expressions using `visitor.claims.unsigned.<claim-name>`.
*/
unsignedClaims?: Record<string, unknown>;
};
};
export type GitBookClient = {
/**
* Get the URL for a GitBook frame.
*/
getFrameURL: (options: GetFrameURLOptions) => string;
/**
* Create a new GitBook frame.
*/
createFrame: (iframe: HTMLIFrameElement) => GitBookFrameClient;
};
export function createGitBook(options: CreateGitBookOptions) {
const client: GitBookClient = {
getFrameURL: (frameOptions) => {
const url = new URL(options.siteURL);
url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`;
if (frameOptions.colorScheme) {
url.searchParams.set('theme', frameOptions.colorScheme);
}
if (frameOptions.visitor?.token) {
url.searchParams.set('jwt_token', frameOptions.visitor.token);
}
if (frameOptions.visitor?.unsignedClaims) {
Object.entries(frameOptions.visitor.unsignedClaims).forEach(([key, value]) => {
url.searchParams.set(`visitor.${key}`, String(value));
});
}
return url.toString();
},
createFrame: (iframe) => createGitBookFrame(iframe),
};
return client;
}
@@ -1,115 +0,0 @@
import { createChannel } from 'bidc';
import type {
FrameToParentMessage,
GitBookEmbeddableConfiguration,
ParentToFrameMessage,
} from './protocol';
export type GitBookFrameClient = {
/**
* Navigate to a page in the docs tab.
*
* Accepts the page's path within the site (e.g. `getting-started/quickstart`),
* an absolute path (e.g. `/help-center/integrations`), or its full published URL.
* The target page may live in any space/section of the site — including one other
* than the embed is currently showing — and is resolved to the right space.
*/
navigateToPage: (path: string) => void;
/**
* Navigate to the assistant.
*/
navigateToAssistant: () => void;
/**
* Post a message to the chat.
*/
postUserMessage: (message: string) => void;
/**
* Clear the chat.
*/
clearChat: () => void;
/**
* Set the placeholder settings.
*/
configure: (settings: Partial<GitBookEmbeddableConfiguration>) => void;
/**
* Register an event listener.
*/
on: (event: string, listener: (...args: any[]) => void) => () => void;
};
/**
* Create a client to communicate with the GitBook Assistant frame.
*/
export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClient {
if (!iframe.contentWindow) {
throw new Error('Iframe must have a content window');
}
const allowTokens = iframe.allow
.split(';')
.map((token) => token.trim())
.filter(Boolean);
if (!allowTokens.includes('clipboard-write')) {
iframe.allow = [...allowTokens, 'clipboard-write'].join('; ');
}
const channel = createChannel(iframe.contentWindow);
channel.receive((message: FrameToParentMessage) => {
console.log('[gitbook:embed] received message', message);
if (message.type === 'close') {
const listeners = events.get('close') || [];
if (listeners) {
listeners.forEach((listener) => listener());
}
}
});
const sendToFrame = (message: ParentToFrameMessage) => {
console.log('[gitbook:embed] send message', message);
channel.send(message);
};
const events = new Map<string, Array<(...args: any[]) => void>>();
const configuration: GitBookEmbeddableConfiguration = {
tabs: ['assistant', 'search', 'docs'],
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
trademark: true,
};
return {
navigateToPage: (pagePath) => {
sendToFrame({ type: 'navigateToPage', pagePath });
},
navigateToAssistant: () => {
sendToFrame({ type: 'navigateToAssistant' });
},
postUserMessage: (message) => sendToFrame({ type: 'postUserMessage', message }),
configure: (settings) => {
Object.assign(configuration, settings);
sendToFrame({ type: 'configure', settings: configuration });
},
clearChat: () => sendToFrame({ type: 'clearChat' }),
on: (event, listener) => {
const listeners = events.get(event) || [];
listeners.push(listener);
events.set(event, listeners);
return () => {
events.set(
event,
listeners.filter((l) => l !== listener)
);
};
},
};
}
-3
View File
@@ -1,3 +0,0 @@
export * from './createGitBook';
export * from './createGitBookFrame';
export * from './protocol';
-116
View File
@@ -1,116 +0,0 @@
import type { AIToolCallResult, AIToolDefinition } from '@gitbook/api';
import type { IconName } from '@gitbook/icons';
/**
* Custom tool definition to be passed to the AI assistant.
*/
export type GitBookToolDefinition = AIToolDefinition & {
/**
* Confirmation action to be displayed to the user before executing the tool.
*/
confirmation?: {
icon?: IconName;
label: string;
};
/**
* Callback when the tool is executed.
* The input is provided by the AI assistant following the input schema of the tool.
*/
execute: (input: object) => Promise<Pick<AIToolCallResult, 'output' | 'summary'>>;
};
/**
* Custom button definition to be passed to the embeddable GitBook.
*/
export type GitBookEmbeddableActionDefinition = {
/**
* Icon to be displayed in the button.
*/
icon: IconName;
/**
* Label to be displayed in the button.
*/
label: string;
/**
* Callback when the button is clicked.
*/
onClick: () => void | Promise<void>;
};
/**
* Overall configuration for the layout of the GitBook embed.
*/
export type GitBookEmbeddableConfiguration = {
/** Tabs to display in the embed (if enabled on the site). */
tabs: ('assistant' | 'docs' | 'search')[];
/** Additional buttons to be displayed in the header of the GitBook embed. */
actions: GitBookEmbeddableActionDefinition[];
/**
* Additional buttons to be displayed in the header of the GitBook embed.
* @deprecated Use `actions` instead.
*/
buttons?: GitBookEmbeddableActionDefinition[];
/** Message to be displayed in the welcome page. */
greeting: {
title: string;
subtitle: string;
};
/**
* Override the assistant name displayed in the UI.
* Limited to 32 characters.
*/
assistantName?: string;
/** Suggestions of questions to be displayed in the welcome page. */
suggestions: string[];
/** Tools to be provided to the assistant. */
tools: GitBookToolDefinition[];
/**
* Display GitBook branding in the embed.
*/
trademark?: boolean;
/**
* Display a close button inside the assistant.
*/
closeButton?: boolean;
};
/**
* Messages sent from the parent to the frame.
*/
export type ParentToFrameMessage =
| {
type: 'postUserMessage';
message: string;
}
| {
type: 'clearChat';
}
| {
type: 'configure';
settings: GitBookEmbeddableConfiguration;
}
| {
type: 'navigateToPage';
pagePath: string;
}
| {
type: 'navigateToAssistant';
};
/**
* Messages sent from the frame to the parent.
*/
export type FrameToParentMessage = {
type: 'close';
};
-2
View File
@@ -1,2 +0,0 @@
export * from './client';
export * from './standalone';
-84
View File
@@ -1,84 +0,0 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import type {
GetFrameURLOptions,
GitBookEmbeddableConfiguration,
GitBookFrameClient,
} from '../client';
import { useGitBook } from './GitBookProvider';
export type GitBookFrameProps = {
className?: string;
} & GetFrameURLOptions &
Partial<GitBookEmbeddableConfiguration>;
/**
* Render a frame with the GitBook Assistant in it.
*/
export function GitBookFrame(props: GitBookFrameProps) {
const {
className,
colorScheme,
visitor,
actions = [],
greeting,
suggestions = [],
tools = [],
tabs = ['assistant', 'search', 'docs'],
trademark = true,
closeButton = false,
assistantName,
} = props;
const frameRef = useRef<HTMLIFrameElement>(null);
const gitbook = useGitBook();
const [gitbookFrame, setGitbookFrame] = useState<GitBookFrameClient | null>(null);
const frameURL = useMemo(
() => gitbook.getFrameURL({ visitor, colorScheme }),
[gitbook, visitor, colorScheme]
);
useEffect(() => {
if (frameRef.current) {
setGitbookFrame(gitbook.createFrame(frameRef.current));
}
}, [gitbook]);
useEffect(() => {
gitbookFrame?.configure({
tabs,
actions,
greeting,
suggestions,
tools,
closeButton,
trademark,
assistantName,
});
}, [
gitbookFrame,
actions,
greeting,
suggestions,
tools,
tabs,
closeButton,
trademark,
assistantName,
]);
return (
<iframe
title="GitBook"
ref={frameRef}
src={frameURL}
width="100%"
height="100%"
allow="clipboard-write"
className={className}
style={colorScheme ? { colorScheme } : undefined}
/>
);
}
@@ -1,36 +0,0 @@
'use client';
import * as React from 'react';
import { type CreateGitBookOptions, createGitBook } from '../client';
import { GitBookContext } from './context';
/**
* Provider for the GitBook client.
*/
export function GitBookProvider(props: React.PropsWithChildren<CreateGitBookOptions>) {
const { siteURL, children } = props;
const options = React.useMemo(
() => ({
siteURL,
}),
[siteURL]
);
const client = React.useMemo(() => createGitBook(options), [options]);
return <GitBookContext.Provider value={client}>{children}</GitBookContext.Provider>;
}
/**
* Hook to access the GitBook client.
*/
export function useGitBook() {
const context = React.useContext(GitBookContext);
if (!context) {
throw new Error('This component must be used within a <GitBookProvider />');
}
return context;
}
-6
View File
@@ -1,6 +0,0 @@
'use client';
import * as React from 'react';
import type { GitBookClient } from '../client';
export const GitBookContext = React.createContext<GitBookClient | null>(null);
-2
View File
@@ -1,2 +0,0 @@
export * from './GitBookProvider';
export * from './GitBookFrame';
-217
View File
@@ -1,217 +0,0 @@
import './style.css';
import {
type CreateGitBookOptions,
type GetFrameURLOptions,
type GitBookClient,
type GitBookEmbeddableConfiguration,
type GitBookFrameClient,
createGitBook,
} from '../client';
export type GitBook = () => void;
type StandaloneCalls =
// Initialize the widget
| ['init', CreateGitBookOptions, GetFrameURLOptions]
// Unload the widget
| ['unload']
// Show the widget
| ['show']
// Hide the widget
| ['hide']
// Open the window
| ['open']
// Close the window
| ['close']
// Toggle the window
| ['toggle']
// Post a user message
| ['postUserMessage', string]
// Clear the chat
| ['clearChat']
// Configure the embed
| ['configure', Partial<GitBookEmbeddableConfiguration & StandaloneConfiguration>]
// Navigate to a page
| ['navigateToPage', string]
// Navigate to the assistant
| ['navigateToAssistant'];
type StandaloneConfiguration = {
/** Configure the button to open the embed */
button: {
/** Label to be displayed in the button. */
label: string;
/** Icon to be displayed in the button. */
icon: 'assistant' | 'sparkle' | 'help' | 'book';
};
};
export type GitBookStandalone = ((...args: StandaloneCalls) => void) & {
q?: StandaloneCalls[];
};
let widgetIframe: HTMLIFrameElement | undefined;
let _client: GitBookClient | undefined;
let _frame: GitBookFrameClient | undefined;
let frameOptions: GetFrameURLOptions | undefined;
let frameConfiguration: GitBookEmbeddableConfiguration & StandaloneConfiguration = {
button: {
label: 'Ask',
icon: 'assistant',
},
actions: [],
greeting: { title: '', subtitle: '' },
suggestions: [],
tools: [],
tabs: ['assistant', 'search', 'docs'],
trademark: true,
};
const widgetButton = document.createElement('button');
widgetButton.id = 'gitbook-widget-button';
widgetButton.addEventListener('click', () => {
GitBook('toggle');
});
widgetButton.innerHTML = `
<span id="gitbook-widget-button-icon" data-icon="${frameConfiguration.button.icon}"></span>
<span id="gitbook-widget-button-label">${frameConfiguration.button.label}</span>
`;
const widgetWindow = document.createElement('div');
widgetWindow.id = 'gitbook-widget-window';
widgetWindow.classList.add('hidden');
document.body.appendChild(widgetButton);
document.body.appendChild(widgetWindow);
function getClient() {
if (!_client) {
throw new Error(
'GitBook client not initialized. Call GitBook("init", { siteURL: "..." }) first.'
);
}
return _client;
}
function getIframe() {
if (!widgetIframe || !_frame) {
const client = getClient();
widgetIframe?.remove();
widgetIframe = document.createElement('iframe');
widgetIframe.id = 'gitbook-widget-iframe';
widgetIframe.allow = 'clipboard-write';
if (frameOptions?.colorScheme) {
widgetIframe.style.colorScheme = frameOptions.colorScheme;
}
widgetIframe.src = client.getFrameURL({
...frameOptions,
});
widgetWindow.appendChild(widgetIframe);
_frame = client.createFrame(widgetIframe);
_frame.on('close', () => {
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
});
}
return { iframe: widgetIframe, frame: _frame };
}
const GitBook = (...args: StandaloneCalls) => {
switch (args[0]) {
case 'init':
if (_client) {
throw new Error(
'GitBook client already initialized. Call GitBook("unload") first.'
);
}
_client = createGitBook(args[1]);
frameOptions = args[2];
break;
case 'unload':
_client = undefined;
_frame = undefined;
widgetIframe?.remove();
widgetWindow.classList.add('hidden');
break;
case 'show':
widgetButton.classList.remove('hidden');
break;
case 'hide':
widgetButton.classList.add('hidden');
break;
case 'open':
widgetWindow.classList.remove('hidden');
widgetButton.classList.add('open');
getIframe();
break;
case 'toggle':
widgetWindow.classList.toggle('hidden');
widgetButton.classList.toggle('open');
getIframe();
break;
case 'close':
widgetWindow.classList.add('hidden');
widgetButton.classList.remove('open');
break;
case 'postUserMessage':
getIframe().frame.postUserMessage(args[1]);
break;
case 'configure': {
const settings = args[1];
// If trademark is disabled, change the (branded) icon to the sparkle icon
if (
settings.trademark === false &&
!settings.button?.icon &&
frameConfiguration.button.icon === 'assistant'
) {
settings.button = {
label: frameConfiguration.button.label,
icon: 'sparkle',
};
}
frameConfiguration = {
...frameConfiguration,
...settings,
};
// Update the button label and icon
if (settings.button?.label) {
const label = widgetButton.querySelector('#gitbook-widget-button-label');
if (label) {
label.textContent = settings.button.label;
}
}
if (settings.button?.icon) {
const icon = widgetButton.querySelector('#gitbook-widget-button-icon');
if (icon) {
icon.setAttribute('data-icon', settings.button.icon);
}
}
getIframe().frame.configure({
...frameConfiguration,
});
break;
}
case 'clearChat':
getIframe().frame.clearChat();
break;
case 'navigateToPage':
getIframe().frame.navigateToPage(args[1]);
break;
case 'navigateToAssistant':
getIframe().frame.navigateToAssistant();
break;
}
};
// @ts-expect-error - GitBook is not defined in the global scope
const precalls = (window.GitBook as GitBookStandalone | undefined)?.q ?? [];
// @ts-expect-error - GitBook is not defined in the global scope
window.GitBook = GitBook;
precalls.forEach((call) => GitBook(...call));
-194
View File
@@ -1,194 +0,0 @@
:root {
--gitbook-widget-top: 1rem;
--gitbook-widget-bottom: 1rem;
--gitbook-widget-right: 1rem;
--gitbook-widget-left: 1rem;
--gitbook-widget-button-height: 46px;
--gitbook-widget-radius: .5rem;
--gitbook-widget-text-size: 1rem;
--gitbook-widget-text-color: #656973;
--gitbook-widget-border-color: #e5e5e5;
--gitbook-widget-background-translucent: rgba(255, 255, 255, 0.9);
--gitbook-widget-background-translucent-hover: rgba(250, 250, 250, 0.9);
--gitbook-widget-background-solid: #FFFFFF;
--gitbook-widget-background-solid-hover: #FBFBFB;
--gitbook-widget-icon-size: 1.25rem;
--gitbook-widget-window-width: 28rem; /* 448px */
--gitbook-widget-window-height: 40rem; /* 640px */
--gitbook-widget-window-spacing: .5rem; /* Spacing between the button and the window */
--gitbook-widget-window-bottom: calc(var(--gitbook-widget-bottom) + var(--gitbook-widget-button-height) + var(--gitbook-widget-window-spacing));
--gitbook-widget-transition-duration-fast: 0.2s;
--gitbook-widget-transition-duration-slow: 0.5s;
--gitbook-widget-easing: cubic-bezier(0.25, 1, 0.5, 1);
--gitbook-widget-easing-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
@media (prefers-color-scheme: dark) {
:root {
--gitbook-widget-text-color: #FFFFFF;
--gitbook-widget-border-color: #202020;
--gitbook-widget-background-translucent: rgba(15, 15, 15, 0.9);
--gitbook-widget-background-translucent-hover: rgba(20, 20, 20, 0.9);
--gitbook-widget-background-solid: #f0f0f0;
}
}
* {
box-sizing: border-box;
}
/* Button */
#gitbook-widget-button {
position: fixed;
bottom: var(--gitbook-widget-bottom);
right: var(--gitbook-widget-right);
height: var(--gitbook-widget-button-height);
z-index: 9999;
display: flex;
align-items: center;
flex-direction: row-reverse;
gap: .5rem;
padding: .75rem 1rem;
border-radius: 100px;
border: 1px solid var(--gitbook-widget-border-color);
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(16px);
font-size: var(--gitbook-widget-text-size);
color: var(--gitbook-widget-text-color);
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
transition: all var(--gitbook-widget-transition-duration-fast) var(--gitbook-widget-easing);
cursor:pointer;
animation: gitbook-widget-present var(--gitbook-widget-transition-duration-slow) var(--gitbook-widget-easing-bounce);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
background-color: var(--gitbook-widget-background-translucent-hover);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
}
#gitbook-widget-button:active {
transform: translateY(0);
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.05), 0 1px 2px -1px rgba(0,0,0,0.05);
}
@media (prefers-contrast: more) {
#gitbook-widget-button {
background-color: var(--gitbook-widget-background-solid);
}
#gitbook-widget-button:hover, #gitbook-widget-button:focus-visible {
background-color: var(--gitbook-widget-background-solid-hover);
}
}
#gitbook-widget-button.hidden {
display: none;
}
#gitbook-widget-button.open {
padding: .75rem;
gap: 0;
}
/* Button: Icon */
#gitbook-widget-button-icon {
width: var(--gitbook-widget-icon-size);
height: var(--gitbook-widget-icon-size);
mask-image: url("https://static-2v.gitbook.com/~gitbook/static/icons/svgs/custom-icons/gitbook-assistant.svg?v=2");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
background-color: currentColor;
}
#gitbook-widget-button-icon[data-icon="sparkle"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/sparkle.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="help"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/circle-question.svg?v=2&token=a463935e93");
}
#gitbook-widget-button-icon[data-icon="book"] {
mask-image: url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/book-open.svg?v=2&token=a463935e93");
}
#gitbook-widget-button.open #gitbook-widget-button-icon {
mask-image: url('https://ka-p.fontawesome.com/releases/v6.6.0/svgs/regular/close.svg?v=2&token=a463935e93');
}
/* Button: Label */
#gitbook-widget-button.open #gitbook-widget-button-label {
display: none;
}
/* Window */
#gitbook-widget-window {
position: fixed;
bottom: var(--gitbook-widget-window-bottom);
right: var(--gitbook-widget-right);
z-index: 9998;
width: calc(min(var(--gitbook-widget-window-width), calc(100vw - var(--gitbook-widget-right) - var(--gitbook-widget-left))));
height: calc(min(var(--gitbook-widget-window-height), calc(100vh - var(--gitbook-widget-window-bottom) - var(--gitbook-widget-top))));
background-color: var(--gitbook-widget-background-translucent);
backdrop-filter: blur(48px);
border: 1px solid var(--gitbook-widget-border-color);
border-radius: var(--gitbook-widget-radius);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
overflow: hidden;
transition-property: transform, opacity, display;
transition-duration: var(--gitbook-widget-transition-duration-slow);
transition-timing-function: var(--gitbook-widget-easing-bounce);
transform-origin: bottom right;
transition-behavior: allow-discrete;
}
@starting-style {
#gitbook-widget-window {
opacity: 0;
transform: scale(0.9);
}
}
body:has(#gitbook-widget-button.hidden) #gitbook-widget-window {
bottom: var(--gitbook-widget-bottom);
}
#gitbook-widget-window.hidden {
transition-property: transform, opacity, display;
transition-duration: var(--gitbook-widget-transition-duration-fast);
transition-timing-function: var(--gitbook-widget-easing);
transition-behavior: allow-discrete;
display: none;
opacity: 0;
transform: scale(0.9);
}
#gitbook-widget-iframe {
width: 100%;
height: 100%;
border: none;
}
@keyframes gitbook-widget-present {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: 'src/index.ts',
outDir: 'dist',
},
{
entry: 'src/react/index.ts',
outDir: 'dist/react',
unbundle: true,
},
]);

Some files were not shown because too many files have changed in this diff Show More