mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-30 05:58:31 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8cfb10974 | |||
| 3cdba0ae8e | |||
| 83e02b621a | |||
| cb2cc52c26 | |||
| 0e15229c52 | |||
| 8547867c6d |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Fix images in reusable content across spaces.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix openapi-select hover in responses
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Respect fullWidth and defaultWidth for images
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix nested a tag causing hydration error
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
fix href being empty in TOC
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix navigation between sections/variants when previewing a site in v2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gitbook/colors": patch
|
||||
---
|
||||
|
||||
Update chroma ratio per step
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Add authorization header for OAuth2
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Indent JSON python code sample
|
||||
Fix openapi CR preview
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
add a force-revalidate api route to force bust the cache in case of errors
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Adds AI sidebar with recommendations based on browsing behaviour
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/react-openapi": minor
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Design tweaks to code blocks and OpenAPI pages
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
fix ISR on preview env
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": minor
|
||||
"gitbook": minor
|
||||
---
|
||||
|
||||
Add support for reusable content across spaces.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Change OpenAPI schema-optional from `info` to `tint` color
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix schemas disclosure label causing client error
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix missing title on button to close the announcement banner.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Make TOC height dynamic based on visible header and footer elements
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Improve error messages around undefined site sections.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix bold header links hover color
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
cache fonts and static image used in OGImage in memory
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Increase logging around caching behaviour causing page crashes.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Handle nested deprecated properties in generateSchemaExample
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Change `Dropdown`s to use Radix's `DropdownMenu`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Add border to filled sidebar on gradient theme
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Deduplicate path parameters from OpenAPI spec
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
remove trailing slash from linker
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
"gitbook-v2": patch
|
||||
---
|
||||
|
||||
Revert investigation work around URL caches.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
|
||||
@@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`.
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`.
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/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.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"
|
||||
@@ -28,7 +28,7 @@ inputs:
|
||||
outputs:
|
||||
deployment-url:
|
||||
description: "Deployment URL"
|
||||
value: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
value: ${{ steps.deploy.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -63,8 +63,8 @@ runs:
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
shell: bash
|
||||
|
||||
- name: Upload the DO worker
|
||||
- id: deploy
|
||||
name: Deploy to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
@@ -72,67 +72,10 @@ runs:
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook-v2/openNext/customWorkers/doWrangler.jsonc
|
||||
|
||||
- id: upload_server
|
||||
name: Upload server to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Extract server version worker ID
|
||||
shell: bash
|
||||
id: extract_server_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run updateWrangler scripts
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
|
||||
- id: upload_middleware
|
||||
name: Upload middleware to Cloudflare
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.10.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Extract middleware version worker ID
|
||||
shell: bash
|
||||
id: extract_middleware_version_id
|
||||
run: |
|
||||
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy server and middleware to Cloudflare
|
||||
if: ${{ inputs.deploy == 'true' }}
|
||||
uses: ./.github/actions/gradual-deploy-cloudflare
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
opServiceAccount: ${{ inputs.opServiceAccount }}
|
||||
opItem: ${{ inputs.opItem }}
|
||||
environment: ${{ inputs.environment }}
|
||||
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
|
||||
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
|
||||
deploy: ${{ inputs.deploy }}
|
||||
|
||||
|
||||
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
env:
|
||||
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
|
||||
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
|
||||
run: |
|
||||
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
|
||||
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
|
||||
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
|
||||
| Site | `v1` | `2v` | `2c` |
|
||||
| --- | --- | --- | --- |
|
||||
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.com/docs) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.com/docs) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.com/docs) |
|
||||
| GitBook | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/docs.gitbook.com) |
|
||||
| E2E | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) |
|
||||
edit-mode: replace
|
||||
visual-testing-v1:
|
||||
@@ -200,6 +200,7 @@ jobs:
|
||||
name: Visual Testing v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 10
|
||||
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -262,6 +263,7 @@ jobs:
|
||||
name: Visual Testing Customers v2 (Cloudflare)
|
||||
needs: deploy-v2-cloudflare
|
||||
timeout-minutes: 8
|
||||
if: startsWith(github.head_ref || github.ref_name, 'cloudflare/') || github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<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://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">
|
||||
@@ -66,7 +66,7 @@ bun dev:v2
|
||||
|
||||
examples:
|
||||
|
||||
- http://localhost:3000/url/gitbook.com/docs
|
||||
- http://localhost:3000/url/docs.gitbook.com
|
||||
- http://localhost:3000/url/open-source.gitbook.io/midjourney
|
||||
|
||||
Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser.
|
||||
@@ -150,11 +150,11 @@ See `LICENSE` for more information.
|
||||
</p>
|
||||
|
||||
```md
|
||||
[](https://www.gitbook.com/preview?utm_source=gitbook_readme_badge&utm_medium=organic&utm_campaign=preview_documentation&utm_content=link)
|
||||
[](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"
|
||||
/>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "@gitbook/cache-tags",
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "^0.111.0",
|
||||
"assert-never": "^1.2.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"packages/colors": {
|
||||
"name": "@gitbook/colors",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.2",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
@@ -49,9 +49,9 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.12.0",
|
||||
"version": "0.11.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
@@ -141,15 +141,14 @@
|
||||
},
|
||||
"packages/gitbook-v2": {
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.5",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"next": "canary",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
@@ -157,6 +156,7 @@
|
||||
"warn-once": "^0.1.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/cloudflare": "^1.0.0-beta.3",
|
||||
"@types/rison": "^0.0.9",
|
||||
"gitbook": "*",
|
||||
"postcss": "^8",
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "2.1.4",
|
||||
"version": "2.1.3",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.10",
|
||||
"@scalar/openapi-types": "^0.1.9",
|
||||
@@ -202,7 +202,7 @@
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
},
|
||||
@@ -232,7 +232,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.3.0",
|
||||
"version": "1.2.1",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.2.19",
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "0.113.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
},
|
||||
@@ -625,7 +625,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.117.1", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-edyi4Y+Zs/9vdU6CvJfwtVSiEWrKzzlp+sIfsyw8WT+mJWnH9CN+o17nxvRWNmAznU+YZm+Qz7itVbG8k+vSkQ=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.113.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-PWMeAkdm4bHSl3b5OmtcmskZ6qRkkDhauCPybo8sGnjS03O14YAUtubAQiNCKX/uwbs+yiQ8KRPyeIwn+g42yw=="],
|
||||
|
||||
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
|
||||
|
||||
@@ -791,9 +791,9 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.6.4", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-/bn9N/6dVu9+sC7AptaGJylKUzyDDgqe3yKfvUxXOpy7whIq/+3Bw+q2/bilyQg6FcskbCWUt9nLvNf1hAVmfA=="],
|
||||
"@opennextjs/aws": ["@opennextjs/aws@3.5.7", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-YjyHJrkIHI7YwQRCp8GjDOudu86oOc1RiwxvBBpPHrplsS18H4ZmkzGggAKhK6B4myGsJQ/q9kNP2TraoZiNzg=="],
|
||||
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.1.0", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.6.4", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-8DOzpLiewivA1pwRNGVPDXbrF79bzJiHscY+M1tekwvKqEqM26CRYafhrA5IDCeyteNaL9AZD6X4DLuCn/eeYQ=="],
|
||||
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.0-beta.3", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.5.7", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^3.114.3 || ^4.7.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-qKBXQZhUeQ+iGvfJeF7PO30g59LHnPOlRVZd77zxwn6Uc9C+c0LSwo8N28XRIWyQPkY007rKk9pSIxOrP4MHtQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
@@ -3649,7 +3649,7 @@
|
||||
|
||||
"@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
"@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
|
||||
@@ -4077,7 +4077,7 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="],
|
||||
"gitbook-v2/next": ["next@15.4.0-canary.7", "", { "dependencies": { "@next/env": "15.4.0-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.0-canary.7", "@next/swc-darwin-x64": "15.4.0-canary.7", "@next/swc-linux-arm64-gnu": "15.4.0-canary.7", "@next/swc-linux-arm64-musl": "15.4.0-canary.7", "@next/swc-linux-x64-gnu": "15.4.0-canary.7", "@next/swc-linux-x64-musl": "15.4.0-canary.7", "@next/swc-win32-arm64-msvc": "15.4.0-canary.7", "@next/swc-win32-x64-msvc": "15.4.0-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-ZYjT0iu+4osz8XIlr31MuoXaNQKRU75UcwEgNBt93gftoh6tzV2Mebz6sOGeVReYuYUvYlLJJksMBTNcFcPbSA=="],
|
||||
|
||||
"global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
|
||||
|
||||
@@ -4721,55 +4721,49 @@
|
||||
|
||||
"@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.2", "", { "os": "android", "cpu": "arm64" }, "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.19.2", "", { "os": "android", "cpu": "x64" }, "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.2", "", { "os": "linux", "cpu": "arm" }, "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.2", "", { "os": "linux", "cpu": "none" }, "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.2", "", { "os": "linux", "cpu": "x64" }, "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.2", "", { "os": "none", "cpu": "x64" }, "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
"@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
|
||||
|
||||
@@ -4975,23 +4969,23 @@
|
||||
|
||||
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="],
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.4.0-canary.7", "", {}, "sha512-q8S7f2lQti3Y3gcAPzE8Pj8y0EwiWHVyyilMzoLbDPXGVfxlQhXLRiFdy2cDkKN4DyjGZWDeehEtw4huvJAa3Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+TMxUu5CAWNe+UFRc47BZAXQxCRqZfVbGyCldddiog4MorvL7kBxSd1qlmrwI73fRRKtXkHIH1TaeItyxzC9rQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.0-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-veXp8lg/X/7O+pG9BDQ3OizFz3B40v29jsvEWj+ULY/W8Z6+dCSd5XPP2M8fG/gKKKA0D6L0CnnM2Mj0RRSUJw=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KxNGfW7BO0Z5B9rJyl9p7YVjNrxAhu06mH6h1PSdouZG7YMYpdRCconVXeuBI0PEu6g3ywNrOVxZUk1V6G5u0Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-THgXgmP/cC4DsNwvC6uqB90CebB7Ep1KyZajQL3fYKT5V4SWr46yngKLyoyJVeAYWJH908MrWddf7Ya/Zq7cyg=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-kpLB3Jj7fProynQYj2ahFyZlJs0xwm71VzCVrNRu6u7qJGXn6dK5h7+hro8y/y1iqjXWgCLSdxWSHahhWK8XdQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.0-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rnGAKvl4cWPVV9D+SybWOGijm0VmKXyqQ+IN0A6WDgdlYZAZP0ZnJv/rq7DSvuOh19AXS8UpQc88SelXV/3j3Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="],
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-/PRbn//EuR3UGiquk050gqvjxLliEgGBy1Cx9KkpAT7szaHOBj1mDDQmxMTEhRex4i3YfKGJXWn5mLMCveya6Q=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="],
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.0-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7a92XL+DlrbWyycCpQjjQMHOrsA0p+VvS7iA2dyi89Xsq0qtOPzFH0Gb56fsjh6M6BQGFhboOSzjmpjlkMTilQ=="],
|
||||
|
||||
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@
|
||||
"turbo": "^2.5.0",
|
||||
"vercel": "^39.3.0"
|
||||
},
|
||||
"packageManager": "bun@1.2.11",
|
||||
"packageManager": "bun@1.2.8",
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "0.113.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"version": "0.3.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "^0.111.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c3f6b8c: Update chroma ratio per step
|
||||
- 5e975ab: Fix code highlighting for HTTP
|
||||
- f7a3470: Change lightness check for color step 9 to allow input colors with a higher-than-needed contrast
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.2",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
|
||||
@@ -214,11 +214,7 @@ export function colorScale(
|
||||
const targetL =
|
||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||
|
||||
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;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
# cloudflare
|
||||
.open-next
|
||||
.wrangler
|
||||
|
||||
# Symbolic links
|
||||
public
|
||||
|
||||
@@ -1,24 +1,5 @@
|
||||
# gitbook-v2
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 3119066: Add support for reusable content across spaces.
|
||||
- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1c8d9fe: keep data cache in OpenNext between deployment
|
||||
- 778624a: Only resize images with supported extensions.
|
||||
- e6ddc0f: Fix URL in sitemap
|
||||
- 5e975ab: Fix code highlighting for HTTP
|
||||
- e15757d: Fix crash on Cloudflare by using latest stable version of Next.js instead of canary
|
||||
- 634e0b4: Improve error messages around undefined site sections.
|
||||
- 97b7c79: Increase logging around caching behaviour causing page crashes.
|
||||
- 3f29206: Update the regex for validating site redirect
|
||||
- dd043df: Revert investigation work around URL caches.
|
||||
|
||||
## 0.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import type { OpenNextConfig } from '@opennextjs/cloudflare';
|
||||
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
|
||||
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
|
||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||
|
||||
export default {
|
||||
default: {
|
||||
override: {
|
||||
wrapper: 'cloudflare-node',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
export default defineCloudflareConfig({
|
||||
incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
|
||||
tagCache: doShardedTagCache({
|
||||
baseShardSize: 12,
|
||||
regionalCache: true,
|
||||
shardReplication: {
|
||||
numberOfSoftReplicas: 2,
|
||||
numberOfHardReplicas: 1,
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
external: true,
|
||||
override: {
|
||||
wrapper: 'cloudflare-edge',
|
||||
converter: 'edge',
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
dangerous: {
|
||||
enableCacheInterception: true,
|
||||
},
|
||||
edgeExternals: ['node:crypto'],
|
||||
} satisfies OpenNextConfig;
|
||||
}),
|
||||
queue: doQueue,
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
// Only needed to run locally, in prod we'll use the one from do.js
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
return runWithCloudflareRequestContext(request, env, ctx, async () => {
|
||||
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
|
||||
const { handler } = await import(
|
||||
'../../.open-next/server-functions/default/handler.mjs'
|
||||
);
|
||||
|
||||
// - `Request`s are handled by the Next server
|
||||
return handler(request, env, ctx);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
{
|
||||
"main": "default.js",
|
||||
"name": "gitbook-open-v2-server",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
// Just as a test for the preview environment to check that everything works
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"staging": {
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// This worker only purposes it to host the different DO that we will need in the other workers.
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
|
||||
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default {
|
||||
async fetch() {
|
||||
// This worker does not handle any requests, it only provides Durable Objects
|
||||
return new Response('This worker is not meant to handle requests directly', {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"main": "do.js",
|
||||
"name": "gitbook-open-v2-do",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
"STAGE": "production"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache"
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
|
||||
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default class extends WorkerEntrypoint {
|
||||
async fetch(request) {
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
// - `Request`s are handled by the Next server
|
||||
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
|
||||
if (reqOrResp instanceof Response) {
|
||||
return reqOrResp;
|
||||
}
|
||||
|
||||
if (this.env.STAGE !== 'preview') {
|
||||
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
|
||||
reqOrResp.headers.set(
|
||||
'Cloudflare-Workers-Version-Overrides',
|
||||
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
|
||||
);
|
||||
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
// If we are in preview mode, we need to send the request to the preview URL
|
||||
const modifiedUrl = new URL(reqOrResp.url);
|
||||
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
|
||||
const nextRequest = new Request(modifiedUrl, reqOrResp);
|
||||
return fetch(nextRequest, {
|
||||
cf: {
|
||||
cacheEverything: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
{
|
||||
"main": "middleware.js",
|
||||
"name": "gitbook-open-v2",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"assets": {
|
||||
"directory": "../../.open-next/assets",
|
||||
"binding": "ASSETS"
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
|
||||
},
|
||||
"env": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-dev"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"PREVIEW_HOSTNAME": "TO_REPLACE",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-preview"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook-staging.com/*",
|
||||
"zone_name": "gitbook-staging.com"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-staging"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-staging"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-staging-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-staging"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||
// Temporary variable to find the issue once deployed
|
||||
// TODO: remove this once the issue is fixed
|
||||
"DEBUG_CLOUDFLARE": "true",
|
||||
"WORKER_VERSION_ID": "TO_REPLACE",
|
||||
"STAGE": "production"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
},
|
||||
{
|
||||
"pattern": "static-2c.gitbook.com/*",
|
||||
"zone_name": "gitbook.com"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-production"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production"
|
||||
},
|
||||
{
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-production"
|
||||
}
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
"service": "gitbook-x-prod-tail"
|
||||
}
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
},
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
"script_name": "gitbook-open-v2-do-production"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// In this script, we use the args from the cli to update the PREVIEW_URL vars in the wrangler config file for the middleware
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const wranglerConfigPath = path.join(__dirname, '../middlewareWrangler.jsonc');
|
||||
|
||||
const file = fs.readFileSync(wranglerConfigPath, 'utf-8');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
// The versionId is in the format xxx-xxx-xxx-xxx, we need the first part to reconstruct the preview URL
|
||||
const versionId = args[0];
|
||||
|
||||
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
|
||||
let updatedFile = file.replace(
|
||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||
`"PREVIEW_HOSTNAME": "${previewHostname}"`
|
||||
);
|
||||
|
||||
updatedFile = updatedFile.replaceAll(
|
||||
/"WORKER_VERSION_ID": "TO_REPLACE"/g,
|
||||
`"WORKER_VERSION_ID": "${versionId}"`
|
||||
);
|
||||
|
||||
fs.writeFileSync(wranglerConfigPath, updatedFile);
|
||||
@@ -1,202 +0,0 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
IncrementalCache,
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
import type { DurableObjectNamespace, Rpc } from '@cloudflare/workers-types';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET';
|
||||
export const DEFAULT_PREFIX = 'incremental-cache';
|
||||
|
||||
export type KeyOptions = {
|
||||
cacheType?: CacheEntryType;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
|
||||
* the cache operations. It also integrates both R2 and Cache API in a single class.
|
||||
* Having our own, will allow us to customize it in the future if needed.
|
||||
*/
|
||||
class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
protected localCache: Cache | undefined;
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
return trace(
|
||||
{
|
||||
operation: 'openNextIncrementalCacheGet',
|
||||
name: cacheKey,
|
||||
},
|
||||
async (span) => {
|
||||
span.setAttribute('cacheType', cacheType ?? 'cache');
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
try {
|
||||
// Check local cache first if available
|
||||
const localCacheEntry = await localCache.match(this.getCacheUrlKey(cacheKey));
|
||||
if (localCacheEntry) {
|
||||
span.setAttribute('cacheHit', 'local');
|
||||
return localCacheEntry.json();
|
||||
}
|
||||
|
||||
const r2Object = await r2.get(cacheKey);
|
||||
if (!r2Object) return null;
|
||||
|
||||
span.setAttribute('cacheHit', 'r2');
|
||||
return {
|
||||
value: await r2Object.json(),
|
||||
lastModified: r2Object.uploaded.getTime(),
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to get from cache', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key, cacheType);
|
||||
return trace(
|
||||
{
|
||||
operation: 'openNextIncrementalCacheSet',
|
||||
name: cacheKey,
|
||||
},
|
||||
async (span) => {
|
||||
span.setAttribute('cacheType', cacheType ?? 'cache');
|
||||
const localCache = await this.getCacheInstance();
|
||||
|
||||
try {
|
||||
await this.writeToR2(cacheKey, JSON.stringify(value));
|
||||
|
||||
//TODO: Check if there is any places where we don't have tags
|
||||
// Ideally we should always have tags, but in case we don't, we need to decide how to handle it
|
||||
// For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
|
||||
const tags = this.getTagsFromCacheEntry(value) ?? [
|
||||
`build_id/${process.env.NEXT_BUILD_ID}`,
|
||||
];
|
||||
|
||||
// We consider R2 as the source of truth, so we update the local cache
|
||||
// only after a successful R2 write
|
||||
await localCache.put(
|
||||
this.getCacheUrlKey(cacheKey),
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
value,
|
||||
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
||||
// See https://developers.cloudflare.com/workers/reference/security-model/
|
||||
lastModified: Date.now(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
// Cache-Control default to 30 minutes, will be overridden by `revalidate`
|
||||
// In theory we should always get the `revalidate` value
|
||||
'cache-control': `max-age=${value.revalidate ?? 60 * 30}`,
|
||||
'cache-tag': tags.join(','),
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to set to cache', e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
const cacheKey = this.getR2Key(key);
|
||||
return trace(
|
||||
{
|
||||
operation: 'openNextIncrementalCacheDelete',
|
||||
name: cacheKey,
|
||||
},
|
||||
async () => {
|
||||
const r2 = getCloudflareContext().env[BINDING_NAME];
|
||||
const localCache = await this.getCacheInstance();
|
||||
if (!r2) throw new Error('No R2 bucket');
|
||||
|
||||
try {
|
||||
await r2.delete(cacheKey);
|
||||
|
||||
// Here again R2 is the source of truth, so we delete from local cache first
|
||||
await localCache.delete(this.getCacheUrlKey(cacheKey));
|
||||
} catch (e) {
|
||||
console.error('Failed to delete from cache', e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async writeToR2(key: string, value: string): Promise<void> {
|
||||
const env = getCloudflareContext().env as {
|
||||
WRITE_BUFFER: DurableObjectNamespace<
|
||||
Rpc.DurableObjectBranded & {
|
||||
write: (key: string, value: string) => Promise<void>;
|
||||
}
|
||||
>;
|
||||
};
|
||||
const id = env.WRITE_BUFFER.idFromName(key);
|
||||
|
||||
// A stub is a client used to invoke methods on the Durable Object
|
||||
const stub = env.WRITE_BUFFER.get(id);
|
||||
|
||||
await stub.write(key, value);
|
||||
}
|
||||
|
||||
async getCacheInstance(): Promise<Cache> {
|
||||
if (this.localCache) return this.localCache;
|
||||
this.localCache = await caches.open('incremental-cache');
|
||||
return this.localCache;
|
||||
}
|
||||
|
||||
// Utility function to generate keys for R2/Cache API
|
||||
getR2Key(key: string, cacheType: CacheEntryType = 'cache'): string {
|
||||
const hash = createHash('sha256').update(key).digest('hex');
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
/\/+/g,
|
||||
'/'
|
||||
);
|
||||
}
|
||||
|
||||
getCacheUrlKey(cacheKey: string): string {
|
||||
return `http://cache.local/${cacheKey}`;
|
||||
}
|
||||
|
||||
getTagsFromCacheEntry<CacheType extends CacheEntryType>(
|
||||
entry: CacheValue<CacheType>
|
||||
): string[] | undefined {
|
||||
if ('tags' in entry && entry.tags) {
|
||||
return entry.tags;
|
||||
}
|
||||
|
||||
if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) {
|
||||
const rawTags = entry.meta.headers['x-next-cache-tags'];
|
||||
if (typeof rawTags === 'string') {
|
||||
return rawTags.split(',');
|
||||
}
|
||||
}
|
||||
if ('value' in entry) {
|
||||
return entry.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
@@ -1,14 +0,0 @@
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';
|
||||
|
||||
export default {
|
||||
name: 'GitbookISRQueue',
|
||||
send: async (msg) => {
|
||||
return trace({ operation: 'gitbookISRQueueSend', name: msg.MessageBody.url }, async () => {
|
||||
const { ctx } = getCloudflareContext();
|
||||
ctx.waitUntil(doQueue.send(msg));
|
||||
});
|
||||
},
|
||||
} satisfies Queue;
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Queue } from '@opennextjs/aws/types/overrides.js';
|
||||
|
||||
export default {
|
||||
name: 'GitbookISRQueue',
|
||||
send: async (msg) => {
|
||||
// We should never reach this point in the server. If that's the case, we should log it.
|
||||
console.warn('GitbookISRQueue: send called on server side, this should not happen.', msg);
|
||||
},
|
||||
} satisfies Queue;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
|
||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
const originalTagCache = doShardedTagCache({
|
||||
baseShardSize: 12,
|
||||
regionalCache: true,
|
||||
// We can probably increase this value even further
|
||||
regionalCacheTtlSec: 60,
|
||||
shardReplication: {
|
||||
numberOfSoftReplicas: 2,
|
||||
numberOfHardReplicas: 1,
|
||||
regionalReplication: {
|
||||
defaultRegion: 'enam',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'GitbookTagCache',
|
||||
mode: 'nextMode',
|
||||
getLastRevalidated: async (tags: string[]) => {
|
||||
const tagsToCheck = tags.filter(softTagFilter);
|
||||
if (tagsToCheck.length === 0) {
|
||||
// If we reach here, it probably means that there is an issue that we'll need to address.
|
||||
console.warn(
|
||||
'getLastRevalidated - No valid tags to check for last revalidation, original tags:',
|
||||
tags
|
||||
);
|
||||
return 0; // If no tags to check, return 0
|
||||
}
|
||||
return trace(
|
||||
{
|
||||
operation: 'gitbookTagCacheGetLastRevalidated',
|
||||
name: tagsToCheck.join(', '),
|
||||
},
|
||||
async () => {
|
||||
return await originalTagCache.getLastRevalidated(tagsToCheck);
|
||||
}
|
||||
);
|
||||
},
|
||||
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
|
||||
const tagsToCheck = tags.filter(softTagFilter);
|
||||
if (tagsToCheck.length === 0) {
|
||||
// If we reach here, it probably means that there is an issue that we'll need to address.
|
||||
console.warn(
|
||||
'hasBeenRevalidated - No valid tags to check for revalidation, original tags:',
|
||||
tags
|
||||
);
|
||||
return false; // If no tags to check, return false
|
||||
}
|
||||
return trace(
|
||||
{
|
||||
operation: 'gitbookTagCacheHasBeenRevalidated',
|
||||
name: tagsToCheck.join(', '),
|
||||
},
|
||||
async () => {
|
||||
const result = await originalTagCache.hasBeenRevalidated(tagsToCheck, lastModified);
|
||||
return result;
|
||||
}
|
||||
);
|
||||
},
|
||||
writeTags: async (tags: string[]) => {
|
||||
return trace(
|
||||
{
|
||||
operation: 'gitbookTagCacheWriteTags',
|
||||
name: tags.join(', '),
|
||||
},
|
||||
async () => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
console.warn('writeTags - No valid tags to write');
|
||||
return; // If no tags to write, exit early
|
||||
}
|
||||
// Write only the filtered tags
|
||||
await originalTagCache.writeTags(tagsToWrite);
|
||||
}
|
||||
);
|
||||
},
|
||||
} satisfies NextModeTagCache;
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "gitbook-v2",
|
||||
"version": "0.3.0",
|
||||
"version": "0.2.5",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@opennextjs/cloudflare": "1.1.0",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"assert-never": "^1.2.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "^15.3.2",
|
||||
"next": "canary",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@sindresorhus/fnv1a": "^3.1.0",
|
||||
"server-only": "^0.0.1",
|
||||
"warn-once": "^0.1.1"
|
||||
"warn-once": "^0.1.1",
|
||||
"rison": "^0.1.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gitbook": "*",
|
||||
"@opennextjs/cloudflare": "^1.0.0-beta.3",
|
||||
"@types/rison": "^0.0.9",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8"
|
||||
@@ -29,9 +29,7 @@
|
||||
"build:v2": "next build",
|
||||
"start": "next start",
|
||||
"build:v2:cloudflare": "opennextjs-cloudflare build",
|
||||
"dev:v2:cloudflare": "wrangler dev --port 8771 --env preview",
|
||||
"dev:v2:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:v2:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"dev:v2:cloudflare": "wrangler dev --port 8771",
|
||||
"unit": "bun test",
|
||||
"typecheck": "tsc --noEmit"
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ export function getBaseContext(input: {
|
||||
// Create link in the same format for links to other sites/sections.
|
||||
linker.toLinkForContent = (rawURL: string) => {
|
||||
const urlObject = new URL(rawURL);
|
||||
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
|
||||
return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1000
-405
File diff suppressed because it is too large
Load Diff
@@ -4,4 +4,5 @@ export * from './pages';
|
||||
export * from './urls';
|
||||
export * from './errors';
|
||||
export * from './lookup';
|
||||
export * from './proxy';
|
||||
export * from './visitor';
|
||||
|
||||
@@ -1,102 +1,55 @@
|
||||
import { race, tryCatch } from '@/lib/async';
|
||||
import { joinPath, joinPathWithBaseURL } from '@/lib/paths';
|
||||
import { trace } from '@/lib/tracing';
|
||||
import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api';
|
||||
import type { PublishedSiteContentLookup } from '@gitbook/api';
|
||||
import { apiClient } from './api';
|
||||
import { getExposableError } from './errors';
|
||||
import type { DataFetcherResponse } from './types';
|
||||
import { getURLLookupAlternatives, stripURLSearch } from './urls';
|
||||
|
||||
interface LookupPublishedContentByUrlInput {
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook API.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: {
|
||||
url: string;
|
||||
visitorAuthToken: string | null;
|
||||
redirectOnError: boolean;
|
||||
apiToken: string | null;
|
||||
visitorPayload: SiteVisitorPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*/
|
||||
export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'resolvePublishedContentByUrl',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.resolvePublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
...(input.visitorPayload ? { visitor: input.visitorPayload } : {}),
|
||||
redirectOnError: input.redirectOnError,
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint.
|
||||
* To optimize caching, we try multiple lookup alternatives and return the first one that matches.
|
||||
*
|
||||
* @deprecated use resolvePublishedContentByUrl.
|
||||
*
|
||||
*/
|
||||
export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) {
|
||||
return lookupPublishedContentByUrl({
|
||||
url: input.url,
|
||||
fetchLookupAPIResult: ({ url, signal }) => {
|
||||
const api = apiClient({ apiToken: input.apiToken });
|
||||
return trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url,
|
||||
visitorAuthToken: input.visitorPayload.jwtToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type TryCatch<T> = ReturnType<typeof tryCatch<T>>;
|
||||
|
||||
async function lookupPublishedContentByUrl(input: {
|
||||
url: string;
|
||||
fetchLookupAPIResult: (args: {
|
||||
url: string;
|
||||
signal: AbortSignal;
|
||||
}) => TryCatch<Awaited<ReturnType<GitBookAPI['urls']['resolvePublishedContentByUrl']>>>;
|
||||
}): Promise<DataFetcherResponse<PublishedSiteContentLookup>> {
|
||||
const lookupURL = new URL(input.url);
|
||||
const url = stripURLSearch(lookupURL);
|
||||
const lookup = getURLLookupAlternatives(url);
|
||||
|
||||
const result = await race(lookup.urls, async (alternative, { signal }) => {
|
||||
const callResult = await input.fetchLookupAPIResult({
|
||||
url: alternative.url,
|
||||
signal,
|
||||
});
|
||||
const api = await apiClient({ apiToken: input.apiToken });
|
||||
|
||||
const callResult = await trace(
|
||||
{
|
||||
operation: 'getPublishedContentByURL',
|
||||
name: alternative.url,
|
||||
},
|
||||
() =>
|
||||
tryCatch(
|
||||
api.urls.getPublishedContentByUrl(
|
||||
{
|
||||
url: alternative.url,
|
||||
visitorAuthToken: input.visitorAuthToken ?? undefined,
|
||||
redirectOnError: input.redirectOnError,
|
||||
|
||||
// As this endpoint is cached by our API, we version the request
|
||||
// to void getting stale data with missing properties.
|
||||
// this could be improved by ensuring our API cache layer is versioned
|
||||
// or invalidated when needed
|
||||
// @ts-expect-error - cacheVersion is not a real query param
|
||||
cacheVersion: 'v2',
|
||||
},
|
||||
{
|
||||
signal,
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (callResult.error) {
|
||||
if (alternative.primary) {
|
||||
|
||||
@@ -190,6 +190,5 @@ export interface GitBookDataFetcher {
|
||||
output: api.AIOutputFormat;
|
||||
model: api.AIModel;
|
||||
tools?: api.AIToolCapabilities;
|
||||
previousResponseId?: string;
|
||||
}): AsyncGenerator<api.AIStreamResponse, void, unknown>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withLeadingSlash, withTrailingSlash } from '@/lib/paths';
|
||||
import type { PublishedSiteContent } from '@gitbook/api';
|
||||
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
|
||||
import { getProxyRequestIdentifier, isProxyRequest } from './proxy';
|
||||
|
||||
/**
|
||||
* Get the appropriate base path for the visitor authentication cookie.
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
|
||||
describe('checkIsSizableImageURL', () => {
|
||||
it('should return Skip for non-parsable URLs', () => {
|
||||
expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for non-http(s) URLs', () => {
|
||||
expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
|
||||
expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for localhost URLs', () => {
|
||||
expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
|
||||
SizableImageAction.Skip
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
|
||||
});
|
||||
|
||||
it('should return Skip for GitBook image URLs', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
|
||||
SizableImageAction.Skip
|
||||
);
|
||||
});
|
||||
|
||||
it('should return Resize for supported image extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
});
|
||||
|
||||
it('should return Resize for URLs without extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
|
||||
});
|
||||
|
||||
it('should return Passthrough for unsupported image extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle URLs with query parameters correctly', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
|
||||
SizableImageAction.Passthrough
|
||||
);
|
||||
});
|
||||
|
||||
it('should be case-insensitive for extensions', () => {
|
||||
expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
|
||||
SizableImageAction.Resize
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,4 @@
|
||||
import { getExtension } from '@/lib/paths';
|
||||
|
||||
export enum SizableImageAction {
|
||||
Resize = 'resize',
|
||||
Skip = 'skip',
|
||||
Passthrough = 'passthrough',
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developers.cloudflare.com/images/transform-images/#supported-input-formats
|
||||
*/
|
||||
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
import { checkIsHttpURL } from '@/lib/urls';
|
||||
|
||||
/**
|
||||
* Check if an image URL is resizable.
|
||||
@@ -17,27 +6,22 @@ const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
* Skip it for SVGs.
|
||||
* Skip it for GitBook images (to avoid recursion).
|
||||
*/
|
||||
export function checkIsSizableImageURL(input: string): SizableImageAction {
|
||||
export function checkIsSizableImageURL(input: string): boolean {
|
||||
if (!URL.canParse(input)) {
|
||||
return SizableImageAction.Skip;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (input.includes('/~gitbook/image')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = new URL(input);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return SizableImageAction.Skip;
|
||||
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.hostname === 'localhost') {
|
||||
return SizableImageAction.Skip;
|
||||
}
|
||||
if (parsed.pathname.includes('/~gitbook/image')) {
|
||||
return SizableImageAction.Skip;
|
||||
if (!checkIsHttpURL(parsed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extension = getExtension(parsed.pathname).toLowerCase();
|
||||
if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
||||
// If no extension, we consider it resizable.
|
||||
return SizableImageAction.Resize;
|
||||
}
|
||||
|
||||
return SizableImageAction.Passthrough;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
import { GITBOOK_IMAGE_RESIZE_SIGNING_KEY, GITBOOK_IMAGE_RESIZE_URL } from '../env';
|
||||
import type { GitBookLinker } from '../links';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
import { checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||
import { getImageSize } from './resizer';
|
||||
import { type SignatureVersion, generateImageSignature } from './signatures';
|
||||
import type { ImageResizer } from './types';
|
||||
@@ -24,7 +24,7 @@ export function createImageResizer({
|
||||
|
||||
return {
|
||||
getResizedImageURL: (urlInput) => {
|
||||
if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) {
|
||||
if (!checkIsSizableImageURL(urlInput)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function createImageResizer({
|
||||
},
|
||||
|
||||
getImageSize: async (input, options) => {
|
||||
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||
if (!checkIsSizableImageURL(input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { getImageResizingContextId } from './getImageResizingContextId';
|
||||
|
||||
describe('getImageResizingContextId', () => {
|
||||
it('should return proxy identifier for proxy requests', () => {
|
||||
const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world');
|
||||
expect(getImageResizingContextId(proxyRequestURL)).toBe('sites/site_foo');
|
||||
});
|
||||
|
||||
it('should return preview identifier for preview requests', () => {
|
||||
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||
expect(getImageResizingContextId(previewRequestURL)).toBe('site_foo');
|
||||
});
|
||||
|
||||
it('should return host for regular requests', () => {
|
||||
const regularRequestURL = new URL('https://example.com/docs/foo/hello/world');
|
||||
expect(getImageResizingContextId(regularRequestURL)).toBe('example.com');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getPreviewRequestIdentifier, isPreviewRequest } from '@v2/lib/preview';
|
||||
import { getProxyRequestIdentifier, isProxyRequest } from '@v2/lib/proxy';
|
||||
import { getProxyRequestIdentifier, isProxyRequest } from '../data';
|
||||
|
||||
/**
|
||||
* Get the site identifier to use for image resizing for an incoming request.
|
||||
@@ -9,9 +8,6 @@ export function getImageResizingContextId(url: URL): string {
|
||||
if (isProxyRequest(url)) {
|
||||
return getProxyRequestIdentifier(url);
|
||||
}
|
||||
if (isPreviewRequest(url)) {
|
||||
return getPreviewRequestIdentifier(url);
|
||||
}
|
||||
|
||||
return url.host;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'server-only';
|
||||
import assertNever from 'assert-never';
|
||||
import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env';
|
||||
import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
||||
import { checkIsSizableImageURL } from '../checkIsSizableImageURL';
|
||||
import { resizeImageWithCDNCgi } from './cdn-cgi';
|
||||
import { resizeImageWithCFFetch } from './cf-fetch';
|
||||
import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types';
|
||||
@@ -13,7 +13,7 @@ export async function getImageSize(
|
||||
input: string,
|
||||
defaultSize: Partial<CloudflareImageOptions> = {}
|
||||
): Promise<{ width: number; height: number } | null> {
|
||||
if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) {
|
||||
if (!checkIsSizableImageURL(input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,17 +48,13 @@ export async function resizeImage(
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<Response> {
|
||||
const action = checkIsSizableImageURL(input);
|
||||
if (action === SizableImageAction.Skip) {
|
||||
throw new Error(
|
||||
'Cannot resize this image, this function should have never been called on this url'
|
||||
);
|
||||
const parsed = new URL(input);
|
||||
if (parsed.protocol === 'data:') {
|
||||
throw new Error('Cannot resize data: URLs');
|
||||
}
|
||||
|
||||
if (action === SizableImageAction.Passthrough) {
|
||||
return fetch(input, {
|
||||
signal: options.signal,
|
||||
});
|
||||
if (parsed.hostname === 'localhost') {
|
||||
throw new Error('Cannot resize localhost URLs');
|
||||
}
|
||||
|
||||
switch (GITBOOK_IMAGE_RESIZE_MODE) {
|
||||
|
||||
@@ -19,7 +19,7 @@ const siteGitBookIO = createLinker({
|
||||
siteBasePath: '/sitename/',
|
||||
});
|
||||
|
||||
describe('toPathInSpace', () => {
|
||||
describe('toPathInContent', () => {
|
||||
it('should return the correct path', () => {
|
||||
expect(root.toPathInSpace('some/path')).toBe('/some/path');
|
||||
expect(variantInSection.toPathInSpace('some/path')).toBe('/section/variant/some/path');
|
||||
@@ -29,16 +29,6 @@ describe('toPathInSpace', () => {
|
||||
expect(root.toPathInSpace('/some/path')).toBe('/some/path');
|
||||
expect(variantInSection.toPathInSpace('/some/path')).toBe('/section/variant/some/path');
|
||||
});
|
||||
|
||||
it('should remove the trailing slash', () => {
|
||||
expect(root.toPathInSpace('some/path/')).toBe('/some/path');
|
||||
expect(variantInSection.toPathInSpace('some/path/')).toBe('/section/variant/some/path');
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
expect(root.toPathInSpace('')).toBe('');
|
||||
expect(variantInSection.toPathInSpace('')).toBe('/section/variant');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toPathInSite', () => {
|
||||
@@ -46,16 +36,6 @@ describe('toPathInSite', () => {
|
||||
expect(root.toPathInSite('some/path')).toBe('/some/path');
|
||||
expect(siteGitBookIO.toPathInSite('some/path')).toBe('/sitename/some/path');
|
||||
});
|
||||
|
||||
it('should remove the trailing slash', () => {
|
||||
expect(root.toPathInSite('some/path/')).toBe('/some/path');
|
||||
expect(siteGitBookIO.toPathInSite('some/path/')).toBe('/sitename/some/path');
|
||||
});
|
||||
|
||||
it('should not add a trailing slash', () => {
|
||||
expect(root.toPathInSite('')).toBe('');
|
||||
expect(siteGitBookIO.toPathInSite('')).toBe('/sitename');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toRelativePathInSite', () => {
|
||||
|
||||
@@ -93,11 +93,6 @@ export function createLinker(
|
||||
},
|
||||
|
||||
toAbsoluteURL(absolutePath: string): string {
|
||||
// If the path is already a full URL, we return it as is.
|
||||
if (URL.canParse(absolutePath)) {
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
if (!servedOn.host) {
|
||||
return absolutePath;
|
||||
}
|
||||
@@ -128,9 +123,5 @@ export function createLinker(
|
||||
function joinPaths(prefix: string, path: string): string {
|
||||
const prefixPath = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
||||
const suffixPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
return removeTrailingSlash(prefixPath + suffixPath);
|
||||
}
|
||||
|
||||
function removeTrailingSlash(path: string): string {
|
||||
return path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
return prefixPath + suffixPath;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
|
||||
|
||||
describe('isPreviewRequest', () => {
|
||||
it('should return true for preview requests', () => {
|
||||
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||
expect(isPreviewRequest(previewRequestURL)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-preview requests', () => {
|
||||
const nonPreviewRequestURL = new URL('https://example.com/docs/foo/hello/world');
|
||||
expect(isPreviewRequest(nonPreviewRequestURL)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPreviewRequestIdentifier', () => {
|
||||
it('should return the correct identifier for preview requests', () => {
|
||||
const previewRequestURL = new URL('https://preview/site_foo/hello/world');
|
||||
expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Check if the request to the site is a preview request.
|
||||
*/
|
||||
export function isPreviewRequest(requestURL: URL): boolean {
|
||||
return requestURL.host === 'preview';
|
||||
}
|
||||
|
||||
export function getPreviewRequestIdentifier(requestURL: URL): string {
|
||||
// For preview requests, we extract the site ID from the pathname
|
||||
// e.g. https://preview/site_id/...
|
||||
const pathname = requestURL.pathname.slice(1).split('/');
|
||||
return pathname[0];
|
||||
}
|
||||
@@ -10,16 +10,15 @@ import {
|
||||
type ResponseCookies,
|
||||
getPathScopedCookieName,
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorData,
|
||||
getVisitorToken,
|
||||
normalizeVisitorAuthURL,
|
||||
} from '@/lib/visitors';
|
||||
} from '@/lib/visitor-token';
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
import {
|
||||
DataFetcherError,
|
||||
getPublishedContentByURL,
|
||||
getVisitorAuthBasePath,
|
||||
normalizeURL,
|
||||
resolvePublishedContentByUrl,
|
||||
throwIfDataError,
|
||||
} from '@v2/lib/data';
|
||||
import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env';
|
||||
@@ -34,15 +33,6 @@ export const config = {
|
||||
|
||||
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
|
||||
|
||||
/**
|
||||
* Temporary list of hosts to test adaptive content using the new resolution API.
|
||||
*/
|
||||
const ADAPTIVE_CONTENT_HOSTS = [
|
||||
'docs.gitbook.com',
|
||||
'adaptive-docs.gitbook-staging.com',
|
||||
'enriched-content-playground.gitbook-staging.io',
|
||||
];
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
try {
|
||||
const requestURL = new URL(request.url);
|
||||
@@ -95,22 +85,17 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
//
|
||||
const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
|
||||
// @ts-ignore - request typing
|
||||
const visitorToken = getVisitorToken({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: siteRequestURL,
|
||||
});
|
||||
|
||||
const withAPIToken = async (apiToken: string | null) => {
|
||||
const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname)
|
||||
? resolvePublishedContentByUrl
|
||||
: getPublishedContentByURL;
|
||||
const siteURLData = await throwIfDataError(
|
||||
resolve({
|
||||
getPublishedContentByURL({
|
||||
url: siteRequestURL.toString(),
|
||||
visitorPayload: {
|
||||
jwtToken: visitorToken?.token ?? undefined,
|
||||
unsignedClaims,
|
||||
},
|
||||
visitorAuthToken: visitorToken?.token ?? null,
|
||||
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow
|
||||
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
|
||||
// For example when the VA backend signature has changed but the token stored in the cookie is not yet expired.
|
||||
@@ -121,13 +106,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
apiToken,
|
||||
})
|
||||
);
|
||||
|
||||
const cookies: ResponseCookies = visitorParamsCookie
|
||||
? [
|
||||
// If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
|
||||
visitorParamsCookie,
|
||||
]
|
||||
: [];
|
||||
const cookies: ResponseCookies = [];
|
||||
|
||||
//
|
||||
// Handle redirects
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import crypto from 'node:crypto';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
interface JsonBody {
|
||||
// The paths need to be the rewritten one, `res.revalidate` call don't go through the middleware
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Only allow POST requests
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
const signatureHeader = req.headers['x-gitbook-signature'] as string | undefined;
|
||||
if (!signatureHeader) {
|
||||
return res.status(400).json({ error: 'Missing signature header' });
|
||||
}
|
||||
// We cannot use env from `@/v2/lib/env` here as it make it crash because of the import "server-only" in the file.
|
||||
if (process.env.GITBOOK_SECRET) {
|
||||
try {
|
||||
const computedSignature = crypto
|
||||
.createHmac('sha256', process.env.GITBOOK_SECRET)
|
||||
.update(JSON.stringify(req.body))
|
||||
.digest('hex');
|
||||
|
||||
if (computedSignature === signatureHeader) {
|
||||
const results = await Promise.allSettled(
|
||||
(req.body as JsonBody).paths.map((path) => {
|
||||
// biome-ignore lint/suspicious/noConsole: we want to log here
|
||||
console.log(`Revalidating path: ${path}`);
|
||||
return res.revalidate(path);
|
||||
})
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: results.every((result) => result.status === 'fulfilled'),
|
||||
errors: results
|
||||
.filter((result) => result.status === 'rejected')
|
||||
.map((result) => (result as PromiseRejectedResult).reason),
|
||||
});
|
||||
}
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
} catch (error) {
|
||||
console.error('Error during revalidation:', error);
|
||||
return res.status(400).json({ error: 'Invalid request or unable to parse JSON' });
|
||||
}
|
||||
}
|
||||
// If no secret is set, we do not allow revalidation
|
||||
return res.status(403).json({ error: 'Revalidation is disabled' });
|
||||
}
|
||||
@@ -2,11 +2,7 @@
|
||||
"main": ".open-next/worker.js",
|
||||
"name": "gitbook-open-v2",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
"allow_importable_env",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"compatibility_flags": ["nodejs_compat", "allow_importable_env"],
|
||||
"assets": {
|
||||
"directory": ".open-next/assets",
|
||||
"binding": "ASSETS"
|
||||
@@ -14,15 +10,8 @@
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
"IS_PREVIEW": "false"
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"vars": {
|
||||
"IS_PREVIEW": "true"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
@@ -94,14 +83,6 @@
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"vars": {
|
||||
// This is a bit misleading, but it means that we can have 500 concurrent revalidations
|
||||
// This means that we'll have up to 100 durable objects instance running at the same time
|
||||
"MAX_REVALIDATE_CONCURRENCY": "100",
|
||||
// Temporary variable to find the issue once deployed
|
||||
// TODO: remove this once the issue is fixed
|
||||
"DEBUG_CLOUDFLARE": "true"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "open-2c.gitbook.com/*",
|
||||
|
||||
@@ -1,51 +1,5 @@
|
||||
# gitbook
|
||||
|
||||
## 0.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8339e91: Fix images in reusable content across spaces.
|
||||
- 326e28e: Design tweaks to code blocks and OpenAPI pages
|
||||
- 3119066: Add support for reusable content across spaces.
|
||||
- 7d7806d: Pass SVG images through image resizing without resizing them to serve them from optimal host.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- c4ebb3f: Fix openapi-select hover in responses
|
||||
- aed79fd: Decrease rounding of header logo
|
||||
- 42ca7e1: Fix openapi CR preview
|
||||
- e6ddc0f: Fix URL in sitemap
|
||||
- 5e975ab: Fix code highlighting for HTTP
|
||||
- 5d504ff: Fix resolution of links in reusable contents
|
||||
- 95a1f65: Better print layouts: wrap code blocks & force table column auto-sizing
|
||||
- 0499966: Fix invalid sitemap.xml generated with relative URLs instead of absolute ones
|
||||
- 2a805cc: Change OpenAPI schema-optional from `info` to `tint` color
|
||||
- 580101d: Fix schemas disclosure label causing client error
|
||||
- 12a455d: Fix OpenAPI layout issues
|
||||
- 97b7c79: Increase logging around caching behaviour causing page crashes.
|
||||
- 373f18f: Prevent section group popovers from opening on click
|
||||
- 3f29206: Update the regex for validating site redirect
|
||||
- 0c973a3: Always link main logo to the root of the site
|
||||
- ae5f1ab: Change `Dropdown`s to use Radix's `DropdownMenu`
|
||||
- 0e201d5: Add border to filled sidebar on gradient theme
|
||||
- dd043df: Revert investigation work around URL caches.
|
||||
- 89a5816: Fix OpenAPI disclosure label ("Show properties") misalignment on mobile
|
||||
- Updated dependencies [c3f6b8c]
|
||||
- Updated dependencies [d00dc8c]
|
||||
- Updated dependencies [42ca7e1]
|
||||
- Updated dependencies [326e28e]
|
||||
- Updated dependencies [5e975ab]
|
||||
- Updated dependencies [f7a3470]
|
||||
- Updated dependencies [580101d]
|
||||
- Updated dependencies [20ebecb]
|
||||
- Updated dependencies [80cb52a]
|
||||
- Updated dependencies [cb5598d]
|
||||
- Updated dependencies [c6637b0]
|
||||
- Updated dependencies [a3ec264]
|
||||
- @gitbook/colors@0.3.3
|
||||
- @gitbook/openapi-parser@2.1.4
|
||||
- @gitbook/react-openapi@1.3.0
|
||||
|
||||
## 0.11.1
|
||||
|
||||
### Patch Changes
|
||||
@@ -610,7 +564,7 @@
|
||||
- 4cbcc5b: Rollback of scalar modal while fixing perf issue
|
||||
- 3996110: Optimize images rendered in community ads
|
||||
- 133c3e7: Update design of Checkbox to be more consistent and readable
|
||||
- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency
|
||||
- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency
|
||||
- 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations
|
||||
- 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level
|
||||
- b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected`
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
VISITOR_TOKEN_COOKIE,
|
||||
getVisitorAuthCookieName,
|
||||
getVisitorAuthCookieValue,
|
||||
} from '@/lib/visitors';
|
||||
} from '@/lib/visitor-token';
|
||||
|
||||
import { getSiteAPIToken } from '../tests/utils';
|
||||
import {
|
||||
@@ -269,7 +269,7 @@ const testCases: TestsCase[] = [
|
||||
},
|
||||
{
|
||||
name: 'GitBook',
|
||||
contentBaseURL: 'https://gitbook.com/docs/',
|
||||
contentBaseURL: 'https://docs.gitbook.com',
|
||||
tests: [
|
||||
{
|
||||
name: 'Home',
|
||||
|
||||
@@ -168,7 +168,6 @@ export function runTestCases(testCases: TestsCase[]) {
|
||||
new URL(testEntryPathname, testCase.contentBaseURL).toString()
|
||||
)
|
||||
: getTestURL(testEntryPathname);
|
||||
|
||||
if (testEntry.cookies) {
|
||||
await context.addCookies(
|
||||
testEntry.cookies.map((cookie) => ({
|
||||
@@ -346,30 +345,19 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
|
||||
*/
|
||||
async function waitForIcons(page: Page) {
|
||||
await page.waitForFunction(() => {
|
||||
const urlStates: Record<
|
||||
string,
|
||||
{ state: 'pending'; uri: null } | { state: 'loaded'; uri: string }
|
||||
> = (window as any).__ICONS_STATES__ || {};
|
||||
const urlStates: Record<string, 'pending' | 'loaded'> =
|
||||
(window as any).__ICONS_STATES__ || {};
|
||||
(window as any).__ICONS_STATES__ = urlStates;
|
||||
|
||||
const fetchSvgAsDataUri = async (url: string): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch SVG: ${response.status}`);
|
||||
}
|
||||
|
||||
const svgText = await response.text();
|
||||
const encoded = encodeURIComponent(svgText).replace(/'/g, '%27').replace(/"/g, '%22');
|
||||
|
||||
return `data:image/svg+xml;charset=utf-8,${encoded}`;
|
||||
};
|
||||
|
||||
const loadUrl = (url: string) => {
|
||||
// Mark the URL as pending.
|
||||
urlStates[url] = { state: 'pending', uri: null };
|
||||
fetchSvgAsDataUri(url).then((uri) => {
|
||||
urlStates[url] = { state: 'loaded', uri };
|
||||
});
|
||||
urlStates[url] = 'pending';
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
urlStates[url] = 'loaded';
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
|
||||
const icons = Array.from(document.querySelectorAll('svg.gb-icon'));
|
||||
@@ -405,11 +393,18 @@ async function waitForIcons(page: Page) {
|
||||
|
||||
// If the URL is already queued for loading, we return the state.
|
||||
if (urlStates[url]) {
|
||||
if (urlStates[url].state === 'loaded') {
|
||||
if (urlStates[url] === 'loaded') {
|
||||
icon.setAttribute('data-argos-state', 'pending');
|
||||
icon.style.maskImage = `url("${urlStates[url].uri}")`;
|
||||
const bckMaskImage = icon.style.maskImage;
|
||||
const bckDisplay = icon.style.display;
|
||||
icon.style.maskImage = '';
|
||||
icon.style.display = 'none';
|
||||
requestAnimationFrame(() => {
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
icon.style.maskImage = bckMaskImage;
|
||||
icon.style.display = bckDisplay;
|
||||
requestAnimationFrame(() => {
|
||||
icon.setAttribute('data-argos-state', 'loaded');
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.12.0",
|
||||
"version": "0.11.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "env-cmd --silent -f ../../.env.local next dev",
|
||||
@@ -16,7 +16,7 @@
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.117.1",
|
||||
"@gitbook/api": "*",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useVisitedPages } from '../Insights';
|
||||
import { usePageContext } from '../PageContext';
|
||||
import { Emoji } from '../primitives';
|
||||
import { type SuggestedPage, useAdaptiveContext } from './AdaptiveContext';
|
||||
import { streamNextPageSuggestions } from './server-actions/streamNextPageSuggestions';
|
||||
|
||||
export function AINextPageSuggestions() {
|
||||
const { selectedJourney, open } = useAdaptiveContext();
|
||||
|
||||
const currentPage = usePageContext();
|
||||
const visitedPages = useVisitedPages((state) => state.pages);
|
||||
|
||||
const [pages, setPages] = useState<SuggestedPage[]>(selectedJourney?.pages ?? []);
|
||||
const [suggestedPages, setSuggestedPages] = useState<SuggestedPage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
if (selectedJourney?.pages && selectedJourney.pages.length > 0) {
|
||||
setPages(selectedJourney.pages);
|
||||
} else {
|
||||
setPages(suggestedPages);
|
||||
}
|
||||
|
||||
if (suggestedPages.length === 0) {
|
||||
(async () => {
|
||||
const stream = await streamNextPageSuggestions({
|
||||
currentPage: {
|
||||
id: currentPage.pageId,
|
||||
title: currentPage.title,
|
||||
},
|
||||
currentSpace: {
|
||||
id: currentPage.spaceId,
|
||||
},
|
||||
visitedPages: visitedPages,
|
||||
});
|
||||
|
||||
for await (const page of stream) {
|
||||
if (canceled) return;
|
||||
|
||||
setPages((prev) => [...prev, page]);
|
||||
setSuggestedPages((prev) => [...prev, page]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [
|
||||
selectedJourney,
|
||||
currentPage.pageId,
|
||||
currentPage.spaceId,
|
||||
currentPage.title,
|
||||
visitedPages,
|
||||
suggestedPages,
|
||||
]);
|
||||
|
||||
return (
|
||||
open && (
|
||||
<div className="animate-fadeIn">
|
||||
<motion.div className="mb-2 flex flex-row items-start gap-3">
|
||||
<AnimatePresence mode="wait">
|
||||
{selectedJourney?.icon ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
key={selectedJourney.icon}
|
||||
>
|
||||
<Icon
|
||||
icon={selectedJourney.icon as IconName}
|
||||
className="left-0 mt-2 size-6 shrink-0 text-tint-subtle"
|
||||
/>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
<motion.div className={tcls('flex flex-col')} layout="position">
|
||||
<div className="font-semibold text-tint text-xs uppercase tracking-wide">
|
||||
Suggested pages
|
||||
</div>
|
||||
<AnimatePresence mode="wait">
|
||||
{selectedJourney?.label ? (
|
||||
<motion.h5
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
key={selectedJourney.label}
|
||||
className="font-semibold text-base"
|
||||
>
|
||||
{selectedJourney.label}
|
||||
</motion.h5>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<div className="-mb-1.5 flex flex-col gap-1">
|
||||
{Object.assign(Array.from({ length: 5 }), pages).map(
|
||||
(page: SuggestedPage | undefined, index) =>
|
||||
page ? (
|
||||
<Link
|
||||
key={`${selectedJourney?.label}-${page.id}`}
|
||||
className="-mx-2 flex animate-fadeIn gap-2 rounded px-2.5 py-1 transition-all hover:bg-tint-hover hover:text-tint-strong"
|
||||
href={page.href}
|
||||
style={{ animationDelay: `${0.2 + index * 0.05}s` }}
|
||||
>
|
||||
{page.icon ? (
|
||||
<Icon
|
||||
icon={page.icon as IconName}
|
||||
className="mt-0.5 size-4 shrink-0 text-tint-subtle"
|
||||
/>
|
||||
) : null}
|
||||
{page.emoji ? <Emoji code={page.emoji} /> : null}
|
||||
{page.title}
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
key={index}
|
||||
className="my-1 h-5 animate-pulse rounded bg-tint-hover"
|
||||
style={{
|
||||
animationDelay: `${index * 0.2}s`,
|
||||
width: `${((index * 17) % 50) + 50}%`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
import { JOURNEY_COUNT, type Journey, useAdaptiveContext } from './AdaptiveContext';
|
||||
|
||||
export function AIPageJourneySuggestions() {
|
||||
const { journeys, selectedJourney, setSelectedJourney, open } = useAdaptiveContext();
|
||||
|
||||
return (
|
||||
open && (
|
||||
<div className="animate-fadeIn">
|
||||
<div className="mb-2 flex flex-row items-center gap-1 font-semibold text-tint text-xs uppercase tracking-wide">
|
||||
More to explore
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.assign(Array.from({ length: JOURNEY_COUNT }), journeys).map(
|
||||
(journey: Journey | undefined, index) => {
|
||||
const isSelected =
|
||||
journey?.label && journey.label === selectedJourney?.label;
|
||||
const isLoading = !journey || journey?.label === undefined;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
disabled={journey?.label === undefined}
|
||||
className={tcls(
|
||||
'flex flex-col items-center justify-center gap-2 rounded bg-tint px-2 py-4 text-center ring-1 ring-tint-subtle ring-inset transition-all',
|
||||
isLoading
|
||||
? 'h-24 scale-90 animate-pulse'
|
||||
: 'hover:bg-tint-hover hover:text-tint-strong hover:ring-tint active:scale-95',
|
||||
isSelected &&
|
||||
'bg-primary-active text-primary-strong ring-2 ring-primary hover:bg-primary-active hover:ring-primary'
|
||||
)}
|
||||
style={{
|
||||
animationDelay: `${index * 0.2}s`,
|
||||
}}
|
||||
onClick={() =>
|
||||
setSelectedJourney(isSelected ? undefined : journey)
|
||||
}
|
||||
>
|
||||
{journey?.icon ? (
|
||||
<Icon
|
||||
icon={journey.icon as IconName}
|
||||
className="size-4 animate-fadeIn text-tint-subtle [animation-delay:300ms]"
|
||||
/>
|
||||
) : null}
|
||||
{journey?.label ? (
|
||||
<span className="animate-fadeIn leading-tight [animation-delay:400ms]">
|
||||
{journey.label}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useVisitedPages } from '../Insights';
|
||||
import { usePageContext } from '../PageContext';
|
||||
import { streamPageJourneySuggestions } from './server-actions';
|
||||
|
||||
export type SuggestedPage = {
|
||||
id: string;
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: string;
|
||||
emoji?: string;
|
||||
};
|
||||
|
||||
export type Journey = {
|
||||
label: string;
|
||||
icon?: string;
|
||||
pages?: Array<SuggestedPage>;
|
||||
};
|
||||
|
||||
type AdaptiveContextType = {
|
||||
journeys: Journey[];
|
||||
selectedJourney: Journey | undefined;
|
||||
setSelectedJourney: (journey: Journey | undefined) => void;
|
||||
loading: boolean;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const AdaptiveContext = React.createContext<AdaptiveContextType | null>(null);
|
||||
|
||||
export const JOURNEY_COUNT = 4;
|
||||
|
||||
/**
|
||||
* Client side context provider to pass information about the current page.
|
||||
*/
|
||||
export function JourneyContextProvider({
|
||||
children,
|
||||
spaces,
|
||||
}: { children: React.ReactNode; spaces: { id: string; title: string }[] }) {
|
||||
const [journeys, setJourneys] = React.useState<Journey[]>([]);
|
||||
const [selectedJourney, setSelectedJourney] = React.useState<Journey | undefined>(undefined);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [open, setOpen] = React.useState(true);
|
||||
|
||||
const currentPage = usePageContext();
|
||||
const visitedPages = useVisitedPages((state) => state.pages);
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
setJourneys([]);
|
||||
|
||||
(async () => {
|
||||
const stream = await streamPageJourneySuggestions({
|
||||
count: JOURNEY_COUNT,
|
||||
currentPage: {
|
||||
id: currentPage.pageId,
|
||||
title: currentPage.title,
|
||||
},
|
||||
currentSpace: {
|
||||
id: currentPage.spaceId,
|
||||
},
|
||||
allSpaces: spaces,
|
||||
visitedPages,
|
||||
});
|
||||
|
||||
for await (const journey of stream) {
|
||||
if (canceled) return;
|
||||
|
||||
setJourneys((prev) => [...prev, journey]);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [currentPage.pageId, currentPage.spaceId, currentPage.title, visitedPages, spaces]);
|
||||
|
||||
return (
|
||||
<AdaptiveContext.Provider
|
||||
value={{ journeys, selectedJourney, setSelectedJourney, loading, open, setOpen }}
|
||||
>
|
||||
{children}
|
||||
</AdaptiveContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to use the adaptive context.
|
||||
*/
|
||||
export function useAdaptiveContext() {
|
||||
const context = React.useContext(AdaptiveContext);
|
||||
if (!context) {
|
||||
throw new Error('useAdaptiveContext must be used within a AdaptiveContextProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { AINextPageSuggestions } from './AINextPageSuggestions';
|
||||
import { AIPageJourneySuggestions } from './AIPageJourneySuggestions';
|
||||
import { useAdaptiveContext } from './AdaptiveContext';
|
||||
import { AdaptivePaneHeader } from './AdaptivePaneHeader';
|
||||
export function AdaptivePane() {
|
||||
const { open } = useAdaptiveContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex flex-col gap-4 rounded-md straight-corners:rounded-none bg-tint-subtle ring-1 ring-tint-subtle ring-inset transition-all duration-300',
|
||||
open ? 'w-72 px-4 py-4' : 'w-56 px-4 py-3'
|
||||
)}
|
||||
>
|
||||
<AdaptivePaneHeader />
|
||||
<AIPageJourneySuggestions />
|
||||
<AINextPageSuggestions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Button, Loading } from '../primitives';
|
||||
import { useAdaptiveContext } from './AdaptiveContext';
|
||||
|
||||
export function AdaptivePaneHeader() {
|
||||
const { loading, open, setOpen } = useAdaptiveContext();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-3 rounded-md straight-corners:rounded-none transition-all duration-500">
|
||||
<div className="flex grow flex-col">
|
||||
<h4 className="flex items-center gap-1.5 font-semibold ">
|
||||
<Loading className="size-4 text-tint-subtle" busy={loading} />
|
||||
For you
|
||||
</h4>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.h5
|
||||
key={loading ? 'loading' : 'loaded'}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="text-tint-subtle text-xs"
|
||||
>
|
||||
{loading ? 'Basing on your context...' : 'Based on your context'}
|
||||
</motion.h5>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<Button
|
||||
variant="blank"
|
||||
className={tcls('px-2 *:transition-transform', !open && '*:-rotate-45')}
|
||||
iconOnly
|
||||
label="Close"
|
||||
icon="close"
|
||||
onClick={() => setOpen(!open)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type { AIMessage } from '@gitbook/api';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
|
||||
/**
|
||||
* Render a message from the API backend.
|
||||
*/
|
||||
export function MessageView(props: {
|
||||
message: AIMessage;
|
||||
}) {
|
||||
const { message } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{message.steps.map((step, index) => {
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2">
|
||||
<DocumentView
|
||||
document={step.content}
|
||||
context={{
|
||||
mode: 'default',
|
||||
contentContext: undefined,
|
||||
wrapBlocksInSuspense: false,
|
||||
}}
|
||||
style={['space-y-5']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './AIPageLinkSummary';
|
||||
export * from './AdaptiveContext';
|
||||
export * from './AdaptivePane';
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'use server';
|
||||
import {
|
||||
type AIMessageInput,
|
||||
AIModel,
|
||||
type AIStreamResponse,
|
||||
type AIToolCapabilities,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
organizationId,
|
||||
siteId,
|
||||
}: {
|
||||
organizationId: string;
|
||||
siteId: string;
|
||||
},
|
||||
{
|
||||
schema,
|
||||
messages,
|
||||
model = AIModel.Fast,
|
||||
tools = {},
|
||||
}: {
|
||||
schema: z.ZodSchema<T>;
|
||||
messages: AIMessageInput[];
|
||||
model?: AIModel;
|
||||
previousResponseId?: string;
|
||||
tools?: AIToolCapabilities;
|
||||
}
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
organizationId,
|
||||
siteId,
|
||||
input: messages,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
tools,
|
||||
model,
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object' && event.jsonChunk) {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
'use server';
|
||||
import {
|
||||
type AIMessage,
|
||||
AIMessageRole,
|
||||
type AIMessageStep,
|
||||
type AIStreamResponse,
|
||||
} from '@gitbook/api';
|
||||
import type { GitBookBaseContext } from '@v2/lib/context';
|
||||
import type { GitBookDataFetcher } from '@v2/lib/data';
|
||||
import { EventIterator } from 'event-iterator';
|
||||
import type { MaybePromise } from 'p-map';
|
||||
import * as partialJson from 'partial-json';
|
||||
import type { DeepPartial } from 'ts-essentials';
|
||||
import type { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { MessageView } from '../MessageView';
|
||||
|
||||
/**
|
||||
* Get the latest value from a stream and the response id.
|
||||
*/
|
||||
export async function generate<T>(
|
||||
promise: MaybePromise<{
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
}>
|
||||
) {
|
||||
const input = await promise;
|
||||
let value: T | undefined;
|
||||
|
||||
for await (const event of input.stream) {
|
||||
value = event;
|
||||
}
|
||||
|
||||
const { responseId } = await input.response;
|
||||
return {
|
||||
responseId,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of an object using the AI.
|
||||
*/
|
||||
export async function streamGenerateObject<T>(
|
||||
context: GitBookBaseContext,
|
||||
{
|
||||
schema,
|
||||
...input
|
||||
}: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'> & {
|
||||
schema: z.ZodSchema<T>;
|
||||
}
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
...input,
|
||||
output: {
|
||||
type: 'object',
|
||||
schema: zodToJsonSchema(schema),
|
||||
},
|
||||
});
|
||||
|
||||
let json = '';
|
||||
return parseResponse<DeepPartial<T>>(rawStream, (event) => {
|
||||
if (event.type === 'response_object') {
|
||||
json += event.jsonChunk;
|
||||
|
||||
const parsed = partialJson.parse(json, partialJson.ALL);
|
||||
return parsed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the generation of a document.
|
||||
*/
|
||||
export async function streamGenerateDocument(
|
||||
context: GitBookBaseContext,
|
||||
input: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'>
|
||||
) {
|
||||
const rawStream = context.dataFetcher.streamAIResponse({
|
||||
...input,
|
||||
output: {
|
||||
type: 'document',
|
||||
},
|
||||
});
|
||||
|
||||
const message: AIMessage = {
|
||||
id: '',
|
||||
role: AIMessageRole.Assistant,
|
||||
steps: [],
|
||||
};
|
||||
|
||||
const updateProcessingMessageStep = (
|
||||
stepIndex: number,
|
||||
callback: (step: AIMessageStep) => void
|
||||
) => {
|
||||
if (stepIndex > message.steps.length) {
|
||||
throw new Error(
|
||||
`Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
|
||||
);
|
||||
}
|
||||
|
||||
if (message.steps[stepIndex]) {
|
||||
message.steps = [...message.steps];
|
||||
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
||||
callback(message.steps[stepIndex]);
|
||||
} else {
|
||||
message.steps = [
|
||||
...message.steps,
|
||||
{
|
||||
content: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
callback(message.steps[stepIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
return parseResponse<React.ReactNode>(rawStream, (event) => {
|
||||
switch (event.type) {
|
||||
/**
|
||||
* The agent is processing a tool call in a new message.
|
||||
*/
|
||||
case 'response_tool_call': {
|
||||
updateProcessingMessageStep(event.stepIndex, (step) => {
|
||||
step.toolCalls ??= [];
|
||||
step.toolCalls.push(event.toolCall);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* The agent is writing the content of a new message.
|
||||
*/
|
||||
case 'response_reasoning':
|
||||
case 'response_document': {
|
||||
updateProcessingMessageStep(event.stepIndex, (step) => {
|
||||
const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
|
||||
|
||||
step[container] ??= {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
};
|
||||
step[container] = {
|
||||
...step[container],
|
||||
nodes: [...step[container].nodes],
|
||||
};
|
||||
if (event.operation === 'insert') {
|
||||
step[container].nodes.push(...event.blocks);
|
||||
} else {
|
||||
step[container].nodes.splice(
|
||||
-event.blocks.length,
|
||||
event.blocks.length,
|
||||
...event.blocks
|
||||
);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return <MessageView message={message} />;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stream from the API to extract the responseId.
|
||||
*/
|
||||
function parseResponse<T>(
|
||||
responseStream: EventIterator<AIStreamResponse>,
|
||||
parse: (response: AIStreamResponse) => T | undefined
|
||||
): {
|
||||
stream: EventIterator<T>;
|
||||
response: Promise<{ responseId: string }>;
|
||||
} {
|
||||
let resolveResponse: (value: { responseId: string }) => void;
|
||||
const response = new Promise<{ responseId: string }>((resolve) => {
|
||||
resolveResponse = resolve;
|
||||
});
|
||||
|
||||
const stream = new EventIterator<T>((queue) => {
|
||||
(async () => {
|
||||
let foundResponse = false;
|
||||
|
||||
for await (const event of responseStream) {
|
||||
if (event.type === 'response_finish') {
|
||||
foundResponse = true;
|
||||
resolveResponse({ responseId: event.responseId });
|
||||
} else {
|
||||
const parsed = parse(event);
|
||||
if (parsed !== undefined) {
|
||||
queue.push(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundResponse) {
|
||||
throw new Error('No response found');
|
||||
}
|
||||
})().then(
|
||||
() => {
|
||||
queue.stop();
|
||||
},
|
||||
(error) => {
|
||||
queue.fail(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { stream, response };
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './streamLinkPageSummary';
|
||||
export * from './api';
|
||||
export * from './streamPageJourneySuggestions';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole, AIModel } from '@gitbook/api';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
@@ -32,18 +32,23 @@ export async function* streamLinkPageSummary({
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const { stream } = await streamGenerateObject(baseContext, {
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
model: AIModel.Fast,
|
||||
schema: z.object({
|
||||
highlight: z.string().describe('The reason why the user should read the target page.'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
input: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 1. Role
|
||||
const { stream } = await streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
highlight: z
|
||||
.string()
|
||||
.describe('The reason why the user should read the target page.'),
|
||||
// questions: z.array(z.string().describe('The questions to sea')).max(3),
|
||||
}),
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 1. Role
|
||||
You are a contextual fact extractor. Your job is to find the exact fact from the linked page that directly answers the implied question in the current paragraph.
|
||||
|
||||
# 2. Task
|
||||
@@ -59,60 +64,60 @@ Extract a contextually-relevant fact that:
|
||||
3. Ensure the fact relates directly to the context of the paragraph containing the link
|
||||
4. Avoid ALL instructional language including words like "use", "click", "select", "create"
|
||||
5. Keep it under 30 words, factual and declarative about what EXISTS or IS TRUE`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 4. Current page
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 4. Current page
|
||||
The content of the current page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: '# 5. Previous pages',
|
||||
},
|
||||
...visitedPages.map(({ spaceId, pageId }) => ({
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Page ${pageId}`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 6. Target page
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpaceId,
|
||||
pageId: currentPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: '# 5. Previous pages',
|
||||
},
|
||||
...visitedPages.map(({ spaceId, pageId }) => ({
|
||||
role: AIMessageRole.Developer,
|
||||
content: `## Page ${pageId}`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId,
|
||||
pageId,
|
||||
},
|
||||
],
|
||||
})),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 6. Target page
|
||||
The content of the target page is:`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 7. Link preview
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: targetSpaceId,
|
||||
pageId: targetPageId,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 7. Link preview
|
||||
The content of the link preview is:
|
||||
> ${linkPreview}
|
||||
> Page ID: ${targetPageId}`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 8. Guidelines & Examples
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `# 8. Guidelines & Examples
|
||||
ALWAYS:
|
||||
- ALWAYS choose facts that directly fulfill the contextual need where the link appears
|
||||
- ALWAYS connect target page information specifically to the current paragraph context
|
||||
@@ -141,13 +146,14 @@ Current paragraph: "Your team mentioned issues with conflicting edits. Need to c
|
||||
Preview: "Live Edit: Real-time collaborative editing."
|
||||
✓ "Teams with GitHub repositories (like yours) cannot use this feature due to sync limitations."
|
||||
✗ "Incompatible with GitHub/GitLab sync and requires specific visibility settings."`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
|
||||
},
|
||||
].filter(filterOutNullable),
|
||||
});
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.User,
|
||||
content: `I'm considering reading the link titled "${linkTitle}" pointing to page ${targetPageId}. Why should I read it? Relate it to the paragraph I'm currently reading.`,
|
||||
},
|
||||
].filter(filterOutNullable),
|
||||
}
|
||||
);
|
||||
|
||||
for await (const value of stream) {
|
||||
const highlight = value.highlight;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
'use server';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
/**
|
||||
* Get a list of pages to read next
|
||||
*/
|
||||
export async function* streamNextPageSuggestions({
|
||||
currentPage,
|
||||
currentSpace,
|
||||
visitedPages,
|
||||
}: {
|
||||
currentPage: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
currentSpace: {
|
||||
id: string;
|
||||
// title: string;
|
||||
};
|
||||
visitedPages?: Array<{ spaceId: string; pageId: string }>;
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const [{ stream }, context] = await Promise.all([
|
||||
streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
pages: z
|
||||
.array(z.string().describe('The IDs of the page to read next.'))
|
||||
.min(5)
|
||||
.max(5),
|
||||
}),
|
||||
tools: {
|
||||
getPages: true,
|
||||
getPageContent: true,
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content:
|
||||
"You are a knowledge navigator. Given the user's visited pages and the documentation's table of contents, suggest a list of pages to read next.",
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The user is in space (ID ${currentSpace.id})`,
|
||||
},
|
||||
// {
|
||||
// role: AIMessageRole.Developer,
|
||||
// content: `Other spaces in the documentation are: ${allSpaces
|
||||
// .map(
|
||||
// (space) => `
|
||||
// - "${space.title}" (ID ${space.id})`
|
||||
// )
|
||||
// .join('\n')}
|
||||
|
||||
// Feel free to create journeys across spaces.`,
|
||||
// },
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The current page is: "${currentPage.title}" (ID ${currentPage.id}). You can use the getPageContent tool to get the content of any relevant links to include in the journey. Only follow links to pages.`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpace.id,
|
||||
pageId: currentPage.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages && visitedPages.length > 0
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The user's visited pages are: ${visitedPages.map((page) => page.pageId).join(', ')}. The content of the last 5 pages are included below.`,
|
||||
attachments: visitedPages.slice(0, 5).map((page) => ({
|
||||
type: 'page' as const,
|
||||
spaceId: page.spaceId,
|
||||
pageId: page.pageId,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
const emitted = new Set<string>();
|
||||
for await (const value of stream) {
|
||||
const pages = value.pages;
|
||||
|
||||
if (!pages) continue;
|
||||
|
||||
for (const pageId of pages) {
|
||||
if (!pageId) continue;
|
||||
if (emitted.has(pageId)) continue;
|
||||
|
||||
emitted.add(pageId);
|
||||
|
||||
const resolvedPage = resolvePageId(context.pages, pageId);
|
||||
if (!resolvedPage) continue;
|
||||
|
||||
yield {
|
||||
id: resolvedPage.page.id,
|
||||
title: resolvedPage.page.title,
|
||||
icon: resolvedPage.page.icon,
|
||||
emoji: resolvedPage.page.emoji,
|
||||
href: context.linker.toPathForPage({
|
||||
pages: context.pages,
|
||||
page: resolvedPage.page,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
'use server';
|
||||
import { type AncestorRevisionPage, resolvePageId } from '@/lib/pages';
|
||||
import { getV1BaseContext } from '@/lib/v1';
|
||||
import { isV2 } from '@/lib/v2';
|
||||
import { AIMessageRole, type RevisionPageDocument } from '@gitbook/api';
|
||||
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
|
||||
import { fetchServerActionSiteContext, getServerActionBaseContext } from '@v2/lib/server-actions';
|
||||
import { z } from 'zod';
|
||||
import { streamGenerateObject } from './api';
|
||||
|
||||
/**
|
||||
* Get a summary of a page, in the context of another page
|
||||
*/
|
||||
export async function* streamPageJourneySuggestions({
|
||||
currentPage,
|
||||
currentSpace,
|
||||
allSpaces,
|
||||
visitedPages,
|
||||
count,
|
||||
}: {
|
||||
currentPage: {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
currentSpace: {
|
||||
id: string;
|
||||
// title: string;
|
||||
};
|
||||
allSpaces: {
|
||||
id: string;
|
||||
title: string;
|
||||
}[];
|
||||
visitedPages?: Array<{ spaceId: string; pageId: string }>;
|
||||
count: number;
|
||||
}) {
|
||||
const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
|
||||
const siteURLData = await getSiteURLDataFromMiddleware();
|
||||
|
||||
const [{ stream }, context] = await Promise.all([
|
||||
streamGenerateObject(
|
||||
baseContext,
|
||||
{
|
||||
organizationId: siteURLData.organization,
|
||||
siteId: siteURLData.site,
|
||||
},
|
||||
{
|
||||
schema: z.object({
|
||||
journeys: z
|
||||
.array(
|
||||
z.object({
|
||||
label: z.string().describe('The label of the journey.'),
|
||||
icon: z
|
||||
.string()
|
||||
.describe(
|
||||
'The icon of the journey. Use an icon from FontAwesome, stripping the `fa-`. Examples: rocket-launch, tennis-ball, cat'
|
||||
),
|
||||
pages: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'A list of pages in the journey, excluding the current page. Try to avoid duplicate content that is very similar.'
|
||||
)
|
||||
.min(5)
|
||||
.max(10),
|
||||
})
|
||||
)
|
||||
.describe('The possible journeys to take through the documentation.')
|
||||
.min(count)
|
||||
.max(count),
|
||||
}),
|
||||
tools: {
|
||||
getPages: true,
|
||||
getPageContent: true,
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content:
|
||||
"You are a knowledge navigator. Given the user's visited pages and the documentation's table of contents, suggest a named journey through the documentation. A journey is a list of pages that are related to each other. A journey's label starts with a verb and has a clear subject. Use sentence case (so only capitalize the first letter of the first word). Be concise and use short words to fit in the label. For example, use 'docs' instead of 'documentation'. Try to pick out specific journeys, not too generic.",
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The user is in space "${allSpaces.find((space) => space.id === currentSpace.id)?.title}" (ID ${currentSpace.id})`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `Other spaces in the documentation are: ${allSpaces
|
||||
.map(
|
||||
(space) => `
|
||||
- "${space.title}" (ID ${space.id})`
|
||||
)
|
||||
.join('\n')}
|
||||
|
||||
Feel free to create journeys across spaces.`,
|
||||
},
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The current page is: "${currentPage.title}" (ID ${currentPage.id}). You can use the getPageContent tool to get the content of any relevant links to include in the journey. Only follow links to pages.`,
|
||||
attachments: [
|
||||
{
|
||||
type: 'page' as const,
|
||||
spaceId: currentSpace.id,
|
||||
pageId: currentPage.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(visitedPages && visitedPages.length > 0
|
||||
? [
|
||||
{
|
||||
role: AIMessageRole.Developer,
|
||||
content: `The user's visited pages are: ${visitedPages.map((page) => page.pageId).join(', ')}. The content of the last 5 pages are included below.`,
|
||||
attachments: visitedPages.slice(0, 5).map((page) => ({
|
||||
type: 'page' as const,
|
||||
spaceId: page.spaceId,
|
||||
pageId: page.pageId,
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
),
|
||||
fetchServerActionSiteContext(baseContext),
|
||||
]);
|
||||
|
||||
const emitted: { label: string; pageIds: string[] }[] = [];
|
||||
const allEmittedPageIds = new Set<string>();
|
||||
|
||||
for await (const value of stream) {
|
||||
const journeys = value.journeys;
|
||||
|
||||
if (!journeys) continue;
|
||||
|
||||
for (const journey of journeys) {
|
||||
if (!journey?.label) continue;
|
||||
if (!journey?.pages || journey.pages?.length === 0) continue;
|
||||
if (emitted.find((item) => item.label === journey.label)) continue;
|
||||
|
||||
const pageIds: string[] = [];
|
||||
const resolvedPages: {
|
||||
page: RevisionPageDocument;
|
||||
ancestors: AncestorRevisionPage[];
|
||||
}[] = [];
|
||||
for (const page of journey.pages) {
|
||||
if (!page) continue;
|
||||
if (!page.id) continue;
|
||||
if (pageIds.includes(page.id)) continue;
|
||||
|
||||
pageIds.push(page.id);
|
||||
|
||||
const resolvedPage = resolvePageId(context.pages, page.id);
|
||||
if (!resolvedPage) continue;
|
||||
|
||||
resolvedPages.push(resolvedPage);
|
||||
}
|
||||
|
||||
emitted.push({
|
||||
label: journey.label,
|
||||
pageIds: pageIds,
|
||||
});
|
||||
|
||||
// Deduplicate pages before yielding
|
||||
const uniquePages = resolvedPages.filter((page) => {
|
||||
if (allEmittedPageIds.has(page.page.id)) {
|
||||
return false;
|
||||
}
|
||||
allEmittedPageIds.add(page.page.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
yield {
|
||||
label: journey.label,
|
||||
icon: journey.icon,
|
||||
pages: uniquePages.map((page) => ({
|
||||
id: page.page.id,
|
||||
title: page.page.title,
|
||||
icon: page.page.icon,
|
||||
emoji: page.page.emoji,
|
||||
href: context.linker.toPathForPage({
|
||||
pages: context.pages,
|
||||
page: page.page,
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import * as storage from '@/lib/local-storage';
|
||||
import type { ResolvedContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
@@ -19,8 +18,6 @@ export function AnnouncementBanner(props: {
|
||||
}) {
|
||||
const { announcement, contentRef } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
|
||||
const hasLink = announcement.link && contentRef?.href;
|
||||
const closeable = announcement.style !== 'danger';
|
||||
|
||||
@@ -28,7 +25,7 @@ export function AnnouncementBanner(props: {
|
||||
const style = BANNER_STYLES[announcement.style];
|
||||
|
||||
return (
|
||||
<div id="announcement-banner" className="theme-bold:bg-header-background pt-4 pb-2">
|
||||
<div className="announcement-banner theme-bold:bg-header-background pt-4 pb-2">
|
||||
<div className="scroll-nojump">
|
||||
<div className={tcls('relative', CONTAINER_STYLE)}>
|
||||
<Tag
|
||||
@@ -84,7 +81,6 @@ export function AnnouncementBanner(props: {
|
||||
className={`absolute top-0 right-4 mt-2 mr-2 rounded straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
|
||||
type="button"
|
||||
onClick={dismissAnnouncement}
|
||||
title={tString(language, 'close')}
|
||||
>
|
||||
<Icon icon="close" className="size-4" />
|
||||
</button>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow(false)}
|
||||
aria-label={tString(language, 'close')}
|
||||
aria-label={tString(language, 'cookies_close')}
|
||||
className={tcls(
|
||||
'absolute',
|
||||
'top-3',
|
||||
@@ -85,7 +85,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
|
||||
'rounded-sm',
|
||||
'hover:bg-tint-hover'
|
||||
)}
|
||||
title={tString(language, 'close')}
|
||||
title={tString(language, 'cookies_close')}
|
||||
>
|
||||
<Icon icon="xmark" className={tcls('size-4')} />
|
||||
</button>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
|
||||
<code
|
||||
id={id}
|
||||
className={tcls(
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line] print:whitespace-pre-wrap',
|
||||
'inline-grid min-w-full grid-cols-[auto_1fr] p-2 [count-reset:line]',
|
||||
withWrap && 'whitespace-pre-wrap'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
--shiki-token-link: theme("colors.primary.10");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.10");
|
||||
--shiki-token-string: theme("colors.warning.10");
|
||||
--shiki-token-string: theme("colors.success.10");
|
||||
--shiki-token-string-expression: theme("colors.success.10");
|
||||
--shiki-token-keyword: theme("colors.danger.10");
|
||||
--shiki-token-parameter: theme("colors.warning.10");
|
||||
@@ -24,7 +24,7 @@
|
||||
--shiki-token-link: theme("colors.primary.11");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.success.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
@@ -41,7 +41,7 @@ html.dark {
|
||||
--shiki-token-comment: theme("colors.neutral.9");
|
||||
|
||||
--shiki-token-constant: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.warning.11");
|
||||
--shiki-token-string: theme("colors.success.11");
|
||||
--shiki-token-string-expression: theme("colors.success.11");
|
||||
--shiki-token-keyword: theme("colors.danger.11");
|
||||
--shiki-token-parameter: theme("colors.warning.11");
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Card } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { getDataOrNull } from '@v2/lib/data';
|
||||
import { Image } from '../utils';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Caption } from './Caption';
|
||||
import { IntegrationBlock } from './Integration';
|
||||
@@ -53,14 +52,7 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
||||
<Card
|
||||
leadingIcon={
|
||||
embed.icon ? (
|
||||
<Image
|
||||
src={embed.icon}
|
||||
className={tcls('w-5', 'h-5')}
|
||||
alt="Logo"
|
||||
sources={{ light: { src: embed.icon } }}
|
||||
sizes={[{ width: 20 }]}
|
||||
resize={context.contentContext.imageResizer}
|
||||
/>
|
||||
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
|
||||
) : null
|
||||
}
|
||||
href={block.data.url}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user