diff --git a/.changeset/nasty-moles-visit.md b/.changeset/nasty-moles-visit.md new file mode 100644 index 000000000..031fb81e3 --- /dev/null +++ b/.changeset/nasty-moles-visit.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +fix ISR on preview env diff --git a/.changeset/slow-lizards-obey.md b/.changeset/slow-lizards-obey.md new file mode 100644 index 000000000..ce1ef0e04 --- /dev/null +++ b/.changeset/slow-lizards-obey.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Make TOC height dynamic based on visible header and footer elements diff --git a/.changeset/thin-buckets-grow.md b/.changeset/thin-buckets-grow.md new file mode 100644 index 000000000..84375c735 --- /dev/null +++ b/.changeset/thin-buckets-grow.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Add `urlObject.hash` to `linker.toLinkForContent` to pass through URL fragment identifiers, used in search diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a65357a23..b93b51896 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -63,9 +63,9 @@ To start your local version of GitBook, run the command `bun dev`. When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/`. -For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/docs.gitbook.com` after running the development server. +For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/gitbook.com/docs` after running the development server. -You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://docs.gitbook.com/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version. +You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version. ### Commit your update diff --git a/.github/composite/deploy-cloudflare/action.yaml b/.github/composite/deploy-cloudflare/action.yaml index a63c69f8e..fbc98fc82 100644 --- a/.github/composite/deploy-cloudflare/action.yaml +++ b/.github/composite/deploy-cloudflare/action.yaml @@ -19,6 +19,12 @@ inputs: deploy: description: 'Deploy as main version for all traffic instead of uploading versions' required: true + commitTag: + description: 'Commit branch to associate with the deployment' + required: true + commitMessage: + description: 'Commit message to associate with the deployment' + required: true outputs: deployment-url: description: "Deployment URL" @@ -49,10 +55,13 @@ runs: GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL + GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL - name: Build worker run: bun run turbo build:v2:cloudflare + env: + GITBOOK_RUNTIME: cloudflare shell: bash - id: deploy name: Deploy to Cloudflare @@ -61,9 +70,9 @@ runs: apiToken: ${{ inputs.apiToken }} accountId: ${{ inputs.accountId }} workingDirectory: ./ - wranglerVersion: '3.112.0' + wranglerVersion: '4.10.0' environment: ${{ inputs.environment }} - command: ${{ fromJSON(inputs.deploy) == true && 'deploy' || 'versions upload' }} --config ./packages/gitbook-v2/wrangler.toml + 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: diff --git a/.github/composite/deploy-vercel/action.yaml b/.github/composite/deploy-vercel/action.yaml index f0e648a4e..74b54e859 100644 --- a/.github/composite/deploy-vercel/action.yaml +++ b/.github/composite/deploy-vercel/action.yaml @@ -54,6 +54,7 @@ runs: GITBOOK_INTEGRATIONS_HOST: ${{ inputs.opItem }}/GITBOOK_INTEGRATIONS_HOST GITBOOK_IMAGE_RESIZE_SIGNING_KEY: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_SIGNING_KEY GITBOOK_IMAGE_RESIZE_URL: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_URL + GITBOOK_IMAGE_RESIZE_MODE: ${{ inputs.opItem }}/GITBOOK_IMAGE_RESIZE_MODE GITBOOK_ASSETS_PREFIX: ${{ inputs.opItem }}/GITBOOK_ASSETS_PREFIX GITBOOK_FONTS_URL: ${{ inputs.opItem }}/GITBOOK_FONTS_URL - name: Build Project Artifacts @@ -62,6 +63,7 @@ runs: env: VERCEL_ORG_ID: ${{ inputs.vercelOrg }} VERCEL_PROJECT_ID: ${{ inputs.vercelProject }} + GITBOOK_RUNTIME: vercel - name: Deploy Project Artifacts to Vercel id: deploy shell: bash diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index e7ce2a6a7..7d8eec610 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -10,7 +10,7 @@ jobs: deploy-v1-cloudflare: name: Deploy v1 to Cloudflare Pages runs-on: ubuntu-latest - environment: + environment: name: ${{ github.ref == 'refs/heads/main' && '1c-production' || '1c-preview' }} url: ${{ steps.deploy.outputs.deployment-url }} permissions: @@ -40,6 +40,7 @@ jobs: run: bun run turbo gitbook#build:cloudflare env: NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} + GITBOOK_RUNTIME: cloudflare - id: deploy name: Deploy to Cloudflare uses: cloudflare/wrangler-action@v3.14.0 @@ -56,7 +57,7 @@ jobs: deploy-v2-vercel: name: Deploy v2 to Vercel (preview) runs-on: ubuntu-latest - environment: + environment: name: 2v-preview url: ${{ steps.deploy.outputs.deployment-url }} outputs: @@ -77,11 +78,11 @@ jobs: deploy-v2-cloudflare: name: Deploy v2 to Cloudflare Worker (preview) runs-on: ubuntu-latest - environment: + environment: name: 2c-preview url: ${{ steps.deploy.outputs.deployment-url }} outputs: - deployment-url: ${{ steps.deploy.outputs.deployment-url }} + deployment-url: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }} steps: - name: Checkout uses: actions/checkout@v4 @@ -95,9 +96,19 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} opItem: op://gitbook-open/2c-preview opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + commitTag: ${{ github.ref == 'refs/heads/main' && 'main' || format('pr{0}', github.event.pull_request.number) }} + commitMessage: ${{ github.sha }} + - name: Extract Worker ID + id: extract-worker-id + if: ${{ !steps.deploy.outputs.deployment-url }} + run: | + if [[ "${{ steps.deploy.outputs.command-output }}" =~ Worker\ Version\ ID:\ ([0-9a-f]{8})-([0-9a-f-]+) ]]; then + WORKER_ID_FIRST_PART="${BASH_REMATCH[1]}" + echo "worker-url=https://${WORKER_ID_FIRST_PART}-gitbook-open-v2-preview.gitbook.workers.dev/" >> $GITHUB_OUTPUT + fi - name: Outputs run: | - echo "URL: ${{ steps.deploy.outputs.deployment-url }}" + echo "URL: ${{ steps.deploy.outputs.deployment-url || steps.extract-worker-id.outputs.worker-url }}" comment-deployments: runs-on: ubuntu-latest name: Comment Deployments (preview) @@ -123,14 +134,14 @@ jobs: body: | Summary of the deployments: - ### Version 1 (production) + ### Version 1 | Version | URL | Status | | --- | --- | --- | | Latest commit | [${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} | | PR | [${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}](${{ needs.deploy-v1-cloudflare.outputs.deployment-alias-url }}) | ${{ needs.deploy-v1-cloudflare.result == 'success' && '✅' || '❌' }} | - ### Version 2 (experimental) + ### Version 2 | Version | URL | Status | | --- | --- | --- | @@ -139,10 +150,10 @@ jobs: ### Test content - | Site | v1 | v2 | - | --- | --- | --- | - | 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) | - | 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) | + | 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) | + | 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: runs-on: ubuntu-latest @@ -184,11 +195,32 @@ jobs: SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/ ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} ARGOS_BUILD_NAME: 'v2-vercel' + visual-testing-v2-cloudflare: + runs-on: ubuntu-latest + name: Visual Testing v2 (Cloudflare) + needs: deploy-v2-cloudflare + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Bun + uses: ./.github/composite/setup-bun + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + - name: Run Playwright tests + run: bun e2e + env: + BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }} + SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/ + ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} + ARGOS_BUILD_NAME: 'v2-cloudflare' visual-testing-customers-v1: runs-on: ubuntu-latest name: Visual Testing Customers v1 needs: deploy-v1-cloudflare - timeout-minutes: 6 + timeout-minutes: 8 steps: - name: Checkout uses: actions/checkout@v4 @@ -208,7 +240,7 @@ jobs: runs-on: ubuntu-latest name: Visual Testing Customers v2 needs: deploy-v2-vercel - timeout-minutes: 6 + timeout-minutes: 8 steps: - name: Checkout uses: actions/checkout@v4 @@ -225,6 +257,27 @@ jobs: SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/ ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} ARGOS_BUILD_NAME: 'customers-v2' + visual-testing-customers-v2-cloudflare: + runs-on: ubuntu-latest + name: Visual Testing Customers v2 (Cloudflare) + needs: deploy-v2-cloudflare + timeout-minutes: 8 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Bun + uses: ./.github/composite/setup-bun + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Setup Playwright + uses: ./.github/actions/setup-playwright + - name: Run Playwright tests + run: bun e2e-customers + env: + BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }} + SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/ + ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} + ARGOS_BUILD_NAME: 'customers-v2' pagespeed-testing-v1: runs-on: ubuntu-latest name: PageSpeed Testing v1 diff --git a/.github/workflows/deploy-production.yaml b/.github/workflows/deploy-production.yaml index 42bae0a3b..9d5bb3abb 100644 --- a/.github/workflows/deploy-production.yaml +++ b/.github/workflows/deploy-production.yaml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Deploy staging + - name: Deploy id: deploy uses: ./.github/composite/deploy-vercel with: @@ -48,6 +48,8 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} opItem: op://gitbook-open/2c-production opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + commitTag: main + commitMessage: ${{ github.sha }} - name: Outputs run: | echo "URL: ${{ steps.deploy.outputs.deployment-url }}" \ No newline at end of file diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index a1e1df62e..ed2f290c7 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -17,7 +17,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Deploy staging + - name: Deploy id: deploy uses: ./.github/composite/deploy-vercel with: @@ -48,6 +48,8 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} opItem: op://gitbook-open/2c-staging opServiceAccount: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + commitTag: main + commitMessage: ${{ github.sha }} - name: Outputs run: | echo "URL: ${{ steps.deploy.outputs.deployment-url }}" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index b9d15df19..8a86e5d5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ ], "tailwindCSS.classAttributes": ["class", "className", "style", ".*Style"], "prettier.enable": false, + "editor.formatOnSave": true, "editor.defaultFormatter": "biomejs.biome", "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit", diff --git a/README.md b/README.md index f8030c085..f9702f88e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

GitBook

- Docs - Community - Developer Docs - Changelog - Bug reports + Docs - Community - Developer Docs - Changelog - Bug reports

@@ -59,15 +59,15 @@ bun install 4. Start your local development server. ``` -bun dev +bun dev:v2 ``` 5. Open a published GitBook space in your web browser, prefixing it with `http://localhost:3000/`. examples: -- http://localhost:3000/docs.gitbook.com -- http://localhost:3000/open-source.gitbook.io/midjourney +- http://localhost:3000/url/gitbook.com/docs +- http://localhost:3000/url/open-source.gitbook.io/midjourney Any published GitBook site can be accessed through your local development instance, and any updates you make to the codebase will be reflected in your browser. diff --git a/biome.json b/biome.json index 0ccfb6e30..f1e55259a 100644 --- a/biome.json +++ b/biome.json @@ -20,7 +20,8 @@ "**/.wrangler/**/*", "packages/openapi-parser/src/fixtures/**/*", "packages/emoji-codepoints/index.ts", - "packages/icons/src/data/*.json" + "packages/icons/src/data/*.json", + "packages/cache-do/worker-configuration.d.ts" ] }, "formatter": { diff --git a/bun.lock b/bun.lock index b65074ce6..631adb8ce 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.27.12", - "turbo": "^2.4.4", + "turbo": "^2.5.0", "vercel": "^39.3.0", }, }, @@ -19,14 +19,14 @@ }, "devDependencies": { "typescript": "^5.5.3", - "wrangler": "^3.112.0", + "wrangler": "^4.10.0", }, }, "packages/cache-tags": { "name": "@gitbook/cache-tags", - "version": "0.2.0", + "version": "0.3.1", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "assert-never": "^1.2.1", }, "devDependencies": { @@ -35,7 +35,7 @@ }, "packages/colors": { "name": "@gitbook/colors", - "version": "0.3.0", + "version": "0.3.3", "devDependencies": { "typescript": "^5.5.3", }, @@ -49,9 +49,9 @@ }, "packages/gitbook": { "name": "gitbook", - "version": "0.8.2", + "version": "0.12.0", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -62,17 +62,18 @@ "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-tooltip": "^1.1.8", "@sindresorhus/fnv1a": "^3.1.0", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.16", - "@upstash/redis": "^1.27.1", - "ai": "^4.1.46", - "ajv": "^8.12.0", + "ai": "^4.2.2", "assert-never": "^1.2.1", "bun-types": "^1.1.20", "classnames": "^2.5.1", + "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", "js-cookie": "^3.0.5", "jsontoxml": "^1.0.1", @@ -81,15 +82,16 @@ "mathjax": "^3.2.2", "mdast-util-to-markdown": "^2.1.2", "memoizee": "^0.4.17", - "next": "14.2.25", + "next": "14.2.26", "next-themes": "^0.2.1", "nuqs": "^2.2.3", "object-hash": "^3.0.0", "openapi-types": "^12.1.3", "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "partial-json": "^0.1.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.4.1", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -104,10 +106,13 @@ "unified": "^11.0.5", "url-join": "^5.0.0", "usehooks-ts": "^3.1.0", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.5", + "zustand": "^5.0.3", }, "devDependencies": { - "@argos-ci/playwright": "^4.3.0", - "@cloudflare/next-on-pages": "1.13.7", + "@argos-ci/playwright": "^5.0.3", + "@cloudflare/next-on-pages": "1.13.12", "@cloudflare/workers-types": "^4.20241230.0", "@playwright/test": "^1.51.1", "@types/js-cookie": "^3.0.6", @@ -136,14 +141,15 @@ }, "packages/gitbook-v2": { "name": "gitbook-v2", - "version": "0.2.2", + "version": "0.3.0", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "1.0.4", "@sindresorhus/fnv1a": "^3.1.0", + "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", - "next": "^15.2.3", - "p-memoize": "^7.1.1", + "next": "^15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", "rison": "^0.1.1", @@ -151,7 +157,6 @@ "warn-once": "^0.1.1", }, "devDependencies": { - "@opennextjs/cloudflare": "^0.5.10", "@types/rison": "^0.0.9", "gitbook": "*", "postcss": "^8", @@ -180,7 +185,7 @@ }, "packages/openapi-parser": { "name": "@gitbook/openapi-parser", - "version": "2.1.1", + "version": "2.1.4", "dependencies": { "@scalar/openapi-parser": "^0.10.10", "@scalar/openapi-types": "^0.1.9", @@ -193,18 +198,11 @@ "typescript": "^5.5.3", }, }, - "packages/proxy": { - "name": "@gitbook/proxy", - "version": "0.1.0", - "devDependencies": { - "typescript": "^5.5.3", - }, - }, "packages/react-contentkit": { "name": "@gitbook/react-contentkit", "version": "0.7.0", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/icons": "workspace:*", "classnames": "^2.5.1", }, @@ -234,11 +232,11 @@ }, "packages/react-openapi": { "name": "@gitbook/react-openapi", - "version": "1.1.6", + "version": "1.3.0", "dependencies": { "@gitbook/openapi-parser": "workspace:*", - "@scalar/api-client-react": "^1.2.5", - "@scalar/oas-utils": "^0.2.120", + "@scalar/api-client-react": "^1.2.19", + "@scalar/oas-utils": "^0.2.130", "clsx": "^2.1.1", "flatted": "^3.2.9", "json-xml-parse": "^1.3.0", @@ -262,50 +260,50 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.106.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "@gitbook/api": "^0.115.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", }, "packages": { - "@ai-sdk/provider": ["@ai-sdk/provider@1.0.9", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.1.10", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "eventsource-parser": "^3.0.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-4GZ8GHjOFxePFzkl3q42AU0DQOtTQ5w09vmaWUf/pKFXJPizlnzKSUkF0f+VkapIUfDugyMqPMT1ge8XQzVI7Q=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.4", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA=="], - "@ai-sdk/react": ["@ai-sdk/react@1.1.19", "", { "dependencies": { "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/ui-utils": "1.1.16", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-zqSOWmJxpB45ZrwZ04+Q7Uo3xeGM0tva2eUYz2T4gv9Yk6MQAfOBA6+scsKg8CyIuUy4M4/C4pCY3eWQf7sfQg=="], + "@ai-sdk/react": ["@ai-sdk/react@1.2.6", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/ui-utils": "1.2.5", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ=="], - "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.1.16", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.0.0" }, "optionalPeers": ["zod"] }, "sha512-jfblR2yZVISmNK2zyNzJZFtkgX57WDAUQXcmn3XUBJyo8LFsADu+/vYMn5AOyBi9qJT0RBk11PEtIxIqvByw3Q=="], + "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.5", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@argos-ci/api-client": ["@argos-ci/api-client@0.8.0", "", { "dependencies": { "debug": "^4.4.0", "openapi-fetch": "0.13.4" } }, "sha512-UHa1vAf8gwHVpkqM/RaSryrFe1juqWH6dHpPeMtT4e/ZMB9hNYwYFinaGq/KRWe88JEi2WeAu776YdoeUSZQkQ=="], + "@argos-ci/api-client": ["@argos-ci/api-client@0.8.1", "", { "dependencies": { "debug": "^4.4.0", "openapi-fetch": "0.13.5" } }, "sha512-3IHv7ANSPNO6OwWgwULlHbP9/tFV9kQDu6+nL9jysfPkGj0GgtrOsyBb+iU931c7wSMo1OD+XNujCnRzDD968w=="], - "@argos-ci/browser": ["@argos-ci/browser@3.2.0", "", {}, "sha512-mHLUamfywbdzrM6SRV4KB4kEQs2lrFxC1yS+QWB/9lME7FKzr7MJW/Q++iWcQli8IbmtLsZ+PqP2eqLaDiRsUw=="], + "@argos-ci/browser": ["@argos-ci/browser@4.1.1", "", {}, "sha512-UyKdnprGftUjWQkb0jqJ0zGHJmcWBzdko8zRy4y+4efukVX4jjC/Px2HvWW8aqwjoR4aplouMZuzhmOkq2SCsA=="], - "@argos-ci/core": ["@argos-ci/core@3.1.0", "", { "dependencies": { "@argos-ci/api-client": "0.8.0", "@argos-ci/util": "2.3.0", "axios": "^1.7.9", "convict": "^6.2.4", "debug": "^4.4.0", "fast-glob": "^3.3.3", "sharp": "^0.33.5", "tmp": "^0.2.3" } }, "sha512-bo/pNKk6P0pz4NRdymgU1letwQrRbMPTeFyMsUEW8fhKNdesSFnFIWZBFGsGkkh05uw75PBjl2ZN4PvQ2TxSog=="], + "@argos-ci/core": ["@argos-ci/core@3.1.1", "", { "dependencies": { "@argos-ci/api-client": "0.8.1", "@argos-ci/util": "2.3.1", "axios": "^1.8.4", "convict": "^6.2.4", "debug": "^4.4.0", "fast-glob": "^3.3.3", "sharp": "^0.33.5", "tmp": "^0.2.3" } }, "sha512-7iE3o1XGxlfHC5AF05pzT0OxuO387sryrZt3gKGj/e+6R20DXz7J49yI8++nQ2cuT+wLhcJp8+X0ox+SGMYHmw=="], - "@argos-ci/playwright": ["@argos-ci/playwright@4.3.0", "", { "dependencies": { "@argos-ci/browser": "3.2.0", "@argos-ci/core": "3.1.0", "@argos-ci/util": "2.3.0", "chalk": "^5.4.1", "debug": "^4.4.0" } }, "sha512-UQBC78zWg+bWXDaevWOL4m6zkrNeprMfdqjy6zagvU9kq7sPQC7nN/qZQ15hs/FT+JpTCa8CbbSAq13eQrmxKQ=="], + "@argos-ci/playwright": ["@argos-ci/playwright@5.0.3", "", { "dependencies": { "@argos-ci/browser": "4.1.1", "@argos-ci/core": "3.1.1", "@argos-ci/util": "2.3.1", "chalk": "^5.4.1", "debug": "^4.4.0" } }, "sha512-sqoARsgnDRrwKm1x10L3Z8+OQukk0F9OksVj9v9rvbzNI2WVCw5zbCUMY0qD4Q3Ba7vMFbl1ELUODRc2mfCbNw=="], - "@argos-ci/util": ["@argos-ci/util@2.3.0", "", {}, "sha512-tkxnCpaj7yN9nCFzo9MX0FJ5YjUepEOGYfdvF8COQqp+EdY1qubOPpc4Z0l1B60BlC8YtjQv/oRxHSh1XzxWFg=="], + "@argos-ci/util": ["@argos-ci/util@2.3.1", "", {}, "sha512-kE61HU2480fbAnimmA4x9HK45ZJvkoqLdW5GxT5uvwhkclQykVd2S6WfGFUr3JokTXfZ5LZEEfoWgtGA316KSQ=="], - "@ast-grep/napi": ["@ast-grep/napi@0.34.3", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.34.3", "@ast-grep/napi-darwin-x64": "0.34.3", "@ast-grep/napi-linux-arm64-gnu": "0.34.3", "@ast-grep/napi-linux-arm64-musl": "0.34.3", "@ast-grep/napi-linux-x64-gnu": "0.34.3", "@ast-grep/napi-linux-x64-musl": "0.34.3", "@ast-grep/napi-win32-arm64-msvc": "0.34.3", "@ast-grep/napi-win32-ia32-msvc": "0.34.3", "@ast-grep/napi-win32-x64-msvc": "0.34.3" } }, "sha512-2yrnMrUw3NVm9hf+YKO+BOY3Aci/qau2vDo0lGtA7qGMma18XPUIOTdzm601k5gPHo4MfxPPZLoe9QdTUviANg=="], + "@ast-grep/napi": ["@ast-grep/napi@0.35.0", "", { "optionalDependencies": { "@ast-grep/napi-darwin-arm64": "0.35.0", "@ast-grep/napi-darwin-x64": "0.35.0", "@ast-grep/napi-linux-arm64-gnu": "0.35.0", "@ast-grep/napi-linux-arm64-musl": "0.35.0", "@ast-grep/napi-linux-x64-gnu": "0.35.0", "@ast-grep/napi-linux-x64-musl": "0.35.0", "@ast-grep/napi-win32-arm64-msvc": "0.35.0", "@ast-grep/napi-win32-ia32-msvc": "0.35.0", "@ast-grep/napi-win32-x64-msvc": "0.35.0" } }, "sha512-3ucaaSxV6fxXoqHrE/rxAvP1THnDdY5jNzGlnvx+JvnY9C/dSRKc0jlRMRz59N3El572+/yNRUUpAV1T9aBJug=="], - "@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.34.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0a8dS+mOP5TYRX3YDiejL1WXWoWga3wpMYZGSs6Ni+SlH1WEO8zyUHe/1z6jNWH8VMHfH9FSCy6+YaPTpiurCA=="], + "@ast-grep/napi-darwin-arm64": ["@ast-grep/napi-darwin-arm64@0.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-T+MN4Oinc+sXjXCIHzfxDDWY7r2pKgPxM6zVeVlkMTrJV2mJtyKYBIS+CABhRM6kflps2T2I6l4DGaKV/8Ym9w=="], - "@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.34.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-n70ha95Fk187B8tgnvR+ZW429EAs/rXktD0839Mdm2+fWjD+JSdB3SADzOGo2cKhuLpLOKnsvfF/bmu+C/p0YQ=="], + "@ast-grep/napi-darwin-x64": ["@ast-grep/napi-darwin-x64@0.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-pEYiN6JI1HY2uWhMYJ9+3yIMyVYKuYdFzeD+dL7odA3qzK0o9N9AM3/NOt4ynU2EhufaWCJr0P5NoQ636qN6MQ=="], - "@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.34.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2LBUvMVkBcO/CJ4ItgZ1MOSqDq2fRmhiLwFxIYjhjG7Rz+4ZNdRY+d7Sl596g0BChB4ffNv+M5HS8uUBuUax1w=="], + "@ast-grep/napi-linux-arm64-gnu": ["@ast-grep/napi-linux-arm64-gnu@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NBuzQngABGKz7lhG08IQb+7nPqUx81Ol37xmS3ZhVSdSgM0mtp93rCbgFTkJcAFE8IMfCHQSg7G4g0Iotz4ABQ=="], - "@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.34.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-vlX4mOyVO1Oy2CdOIFi7HBPwMKzOyLdBpRCcu7pArBOQJkpJ2eS5GR5qSW15f7KPLTkUMpJq7juLz/rP6Rc79Q=="], + "@ast-grep/napi-linux-arm64-musl": ["@ast-grep/napi-linux-arm64-musl@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1EcvHPwyWpCL/96LuItBYGfeI5FaMTRvL+dHbO/hL5q1npqbb5qn+ppJwtNOjTPz8tayvgggxVk9T4C2O7taYA=="], - "@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.34.3", "", { "os": "linux", "cpu": "x64" }, "sha512-F9TwAfZT/vjjxoPH9Fk8/PTNB95Hm2V/rtva+xMCxkTnhaSh0swM+ku3vavkZ4rwk+LfKPAY37pifEVWg4JPNQ=="], + "@ast-grep/napi-linux-x64-gnu": ["@ast-grep/napi-linux-x64-gnu@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-FDzNdlqmQnsiWXhnLxusw5AOfEcEM+5xtmrnAf3SBRFr86JyWD9qsynnFYC2pnP9hlMfifNH2TTmMpyGJW49Xw=="], - "@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.34.3", "", { "os": "linux", "cpu": "x64" }, "sha512-2W0ZYsRxdVnwJ/BfnCOSKgfcZ2UFf5I+vF5aMmeAOplOg7vlFHb8XHSa4GqO0MoBfFTGKTH76bKwxLz8d38y1Q=="], + "@ast-grep/napi-linux-x64-musl": ["@ast-grep/napi-linux-x64-musl@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wlmndjfBafT8u5p4DBnoRQyoCSGNuVSz7rT3TqhvlHcPzUouRWMn95epU9B1LNLyjXvr9xHeRjSktyCN28w57Q=="], - "@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-XRWHlZnO77dEjC7IM5aQRTBC/hc/08Hdl18baJL7smG2dYGJKonUA7BQns6Vt2i63sOEghclkDw6Pq0PD60dbw=="], + "@ast-grep/napi-win32-arm64-msvc": ["@ast-grep/napi-win32-arm64-msvc@0.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gkhJeYc4rrZLX2icLxalPikTLMR57DuIYLwLr9g+StHYXIsGHrbfrE6Nnbdd8Izfs34ArFCrcwdaMrGlvOPSeg=="], - "@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-0jR3QIkuasSrEvsaGtsgMEvgEY8FVe4pemuW77hUOH/mhO4vxFOWny7w4kUaBxQkzJ5z3lbXVoqO4Uv9rpJsRA=="], + "@ast-grep/napi-win32-ia32-msvc": ["@ast-grep/napi-win32-ia32-msvc@0.35.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-OdUuRa3chHCZ65y+qALfkUjz0W0Eg21YZ9TyPquV5why07M6HAK38mmYGzLxFH6294SvRQhs+FA/rAfbKeH0jA=="], - "@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OY1Cswkz+0bRZgt9sLjzgyA+y0X164UFjwf5WTYpbCTPUlwdpJP6L7FqJNRMemzQp0qQwwRR7ejpBUF4o/V0Aw=="], + "@ast-grep/napi-win32-x64-msvc": ["@ast-grep/napi-win32-x64-msvc@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pcQRUHqbroTN1oQ56V982a7IZTUUySQYWa2KEyksiifHGuBuitlzcyzFGjT96ThcqD9XW0UVJMvpoF2Qjh006Q=="], "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], @@ -473,19 +471,21 @@ "@changesets/write": ["@changesets/write@0.3.2", "", { "dependencies": { "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" } }, "sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw=="], - "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], + + "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.12", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-rPy7x9c2+0RDDdJ5o0TeRUwXJ1b7N1epnqF6qKSp5Wz1r9KHOyvaZh1ACoOC6Vu5k9su5WZOgy+8fPLIyrldMQ=="], - "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.7", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-TSMVy+1fmxzeyykOC9guMEj7G2FgENw1T8V1sIFnah6piaJNBmybdyikPdQSdikP5w6v9eQhBt/TrXDPMDw0dw=="], + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250320.0" }, "optionalPeers": ["workerd"] }, "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250409.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-smA9yq77xsdQ1NMLhFz3JZxMHGd01lg0bE+X3dTFmIUs+hHskJ+HJ/IkMFInkCCeEFlUkoL4yO7ilaU/fin/xA=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250409.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oLVcf+Y5Qun8JHcy1VcR/YnbA5U2ne0czh3XNhDqdHZFK8+vKeC7MnVPX+kEqQA3+uLcMM1/FsIDU1U4Na0h1g=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250409.0", "", { "os": "linux", "cpu": "x64" }, "sha512-D31B4kdC3a0RD5yfpdIa89//kGHbYsYihZmejm1k4S4NHOho3MUDHAEh4aHtafQNXbZdydGHlSyiVYjTdQ9ILQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250409.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Sr59P0TREayil5OQ7kcbjuIn6L6OTSRLI91LKu0D8vi1hss2q9FUwBcwxg0+Yd/x+ty/x7IISiAK5QBkAMeITQ=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250409.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dK9I8zBX5rR7MtaaP2AhICQTEw3PVzHcsltN8o46w7JsbYlMvFOj27FfYH5dhs3IahgmIfw2e572QXW2o/dbpg=="], "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20241230.0", "", {}, "sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ=="], @@ -547,53 +547,55 @@ "@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="], - "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], - "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.24.2", "", { "os": "android", "cpu": "x64" }, "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.2", "", { "os": "linux", "cpu": "none" }, "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.2", "", { "os": "none", "cpu": "x64" }, "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="], "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], @@ -623,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.106.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-2qA/w18JwHe2fwR2A45q1pEdCZArxqUloegfNibu0xngDhea+iKTXSBrN94wD6Lwkh7cqBp9MvtxC+YMz3hx1g=="], + "@gitbook/api": ["@gitbook/api@0.115.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Lyj+1WVNnE/Zuuqa/1ZdnUQfUiNE6es89RFK6CJ+Tb36TFwls6mbHKXCZsBwSYyoMYTVK39WQ3Nob6Nw6+TWCA=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], @@ -639,8 +641,6 @@ "@gitbook/openapi-parser": ["@gitbook/openapi-parser@workspace:packages/openapi-parser"], - "@gitbook/proxy": ["@gitbook/proxy@workspace:packages/proxy"], - "@gitbook/react-contentkit": ["@gitbook/react-contentkit@workspace:packages/react-contentkit"], "@gitbook/react-math": ["@gitbook/react-math@workspace:packages/react-math"], @@ -673,6 +673,8 @@ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], @@ -751,25 +753,25 @@ "@msgpack/msgpack": ["@msgpack/msgpack@3.0.0-beta2", "", {}, "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw=="], - "@next/env": ["@next/env@14.2.25", "", {}, "sha512-JnzQ2cExDeG7FxJwqAksZ3aqVJrHjFwZQAEJ9gQZSoEhIow7SNoKZzju/AwQ+PLIR4NY8V0rhcVozx/2izDO0w=="], + "@next/env": ["@next/env@14.2.26", "", {}, "sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.25", "", { "os": "darwin", "cpu": "arm64" }, "sha512-09clWInF1YRd6le00vt750s3m7SEYNehz9C4PUcSu3bAdCTpjIV4aTYQZ25Ehrr83VR1rZeqtKUPWSI7GfuKZQ=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.26", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.25", "", { "os": "darwin", "cpu": "x64" }, "sha512-V+iYM/QR+aYeJl3/FWWU/7Ix4b07ovsQ5IbkwgUK29pTHmq+5UxeDr7/dphvtXEq5pLB/PucfcBNh9KZ8vWbug=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.26", "", { "os": "darwin", "cpu": "x64" }, "sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFnV2899PJZAIEHQ4IMmZIgL0FBieh5keMnriMY1cK7ompR+JUd24xeTtKkcaw8QmxmEdhoE5Mu9dPSuDBgtTg=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.25", "", { "os": "linux", "cpu": "arm64" }, "sha512-QC5y5PPTmtqFExcKWKYgUNkHeHE/z3lUsu83di488nyP0ZzQ3Yse2G6TCxz6nNsQwgAx1BehAJTZez+UQxzLfw=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.26", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-y6/ML4b9eQ2D/56wqatTJN5/JR8/xdObU2Fb1RBidnrr450HLCKr6IJZbPqbv7NXmje61UyxjF5kvSajvjye5w=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.25", "", { "os": "linux", "cpu": "x64" }, "sha512-sPX0TSXHGUOZFvv96GoBXpB3w4emMqKeMgemrSxI7A6l55VBJp/RKYLwZIB9JxSqYPApqiREaIIap+wWq0RU8w=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.26", "", { "os": "linux", "cpu": "x64" }, "sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.25", "", { "os": "win32", "cpu": "arm64" }, "sha512-ReO9S5hkA1DU2cFCsGoOEp7WJkhFzNbU/3VUF6XxNGUCQChyug6hZdYL/istQgfT/GWE6PNIg9cm784OI4ddxQ=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.26", "", { "os": "win32", "cpu": "arm64" }, "sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw=="], - "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.25", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZ/gc0o9neuCDyD5IumyTGHVun2dCox5TfPQI/BJTYwpSNYM3CZDI4i6TOdjeq1JMo+Ug4kPSMuZdwsycwFbAw=="], + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.26", "", { "os": "win32", "cpu": "ia32" }, "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.25", "", { "os": "win32", "cpu": "x64" }, "sha512-KSznmS6eFjQ9RJ1nEc66kJvtGIL1iZMYmGEXsZPh2YtnLtqrgdVvKXJY2ScjjoFnG6nGLyPFR0UiEvDwVah4Tw=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.26", "", { "os": "win32", "cpu": "x64" }, "sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w=="], "@noble/ciphers": ["@noble/ciphers@1.2.1", "", {}, "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA=="], @@ -789,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@https://pkg.pr.new/@opennextjs/aws@756", { "dependencies": { "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0" }, "bin": { "open-next": "./dist/index.js" } }], + "@opennextjs/aws": ["@opennextjs/aws@3.6.2", "", { "dependencies": { "@ast-grep/napi": "^0.35.0", "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.25.4", "express": "5.0.1", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.0.0", "yaml": "^2.7.0" }, "bin": { "open-next": "dist/index.js" } }, "sha512-26/3GSoj7mKN7XpQFikYM2Lrwal5jlMc4fJO//QAdd5bCSnBaWBAkzr7+VvXAFVIC6eBeDLdtlWiQuUQVEAPZQ=="], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@0.5.10", "", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@756", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.111.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-L/D472YT5OW1LwpFtD/aVXHJYcVPbFVX7XdphlUjCR4+2osSQIDnsuNgfDRydHMDJZMKxeZDc251ZBzUVKpCqw=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.0.4", "", { "dependencies": { "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "^3.6.2", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-tqdm": "^0.8.6" }, "peerDependencies": { "wrangler": "^4.14.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DVudGpOSJ91ruPiBJ0kuFmPhEQbXIXLTbvUjAx1OlbwFskG2gvdNIAmF3ZXV6z1VGDO7Q/u2W2ybMZLf7avlrA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -817,11 +819,15 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.12", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.4", "@radix-ui/react-portal": "1.1.6", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-roving-focus": "1.1.7", "@radix-ui/react-slot": "1.2.0", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+qYq6LfbiGo97Zz9fioX83HCiIYYFNs8zAsVCMQrIakoNYylIzWuoD/anAD3UzvvR6cnswmfRFJFq/zYYq/k7Q=="], "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.4", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wUi01RrTDTOoGtjEPHsxlzPtVzVc3R/AZ5wfh0dyqMAqolhHAHvG5iQjBCTi2AjQqa77FWWbA3kE3RkD+bDMgQ=="], @@ -835,12 +841,18 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.0", "", { "dependencies": { "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-C6oAg451/fQT3EGbWHbCQjYTtbyjNO1uzQgMzwyivcHT3GKNEmu1q3UuREhN+HzHAVtv3ivMVK08QlC+PkYw9Q=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.5", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw=="], "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], @@ -1069,39 +1081,39 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], - "@scalar/api-client": ["@scalar/api-client@2.3.5", "", { "dependencies": { "@headlessui/tailwindcss": "^0.2.0", "@headlessui/vue": "^1.7.20", "@scalar/components": "0.13.37", "@scalar/draggable": "0.1.11", "@scalar/icons": "0.1.3", "@scalar/import": "0.3.2", "@scalar/oas-utils": "0.2.120", "@scalar/object-utils": "1.1.13", "@scalar/openapi-parser": "0.10.10", "@scalar/openapi-types": "0.1.9", "@scalar/postman-to-openapi": "0.1.43", "@scalar/snippetz": "0.2.16", "@scalar/themes": "0.9.79", "@scalar/types": "0.1.1", "@scalar/use-codemirror": "0.11.82", "@scalar/use-hooks": "0.1.33", "@scalar/use-toasts": "0.7.9", "@scalar/use-tooltip": "1.0.6", "@vueuse/core": "^10.10.0", "@vueuse/integrations": "^11.2.0", "focus-trap": "^7", "fuse.js": "^7.0.0", "microdiff": "^1.4.0", "nanoid": "^5.0.9", "pretty-bytes": "^6.1.1", "pretty-ms": "^8.0.0", "shell-quote": "^1.8.1", "vue": "^3.5.12", "vue-router": "^4.3.0", "whatwg-mimetype": "^4.0.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-bVBP8H3laa4VG3dExv4Ak/Kf+q9z8aK/Glpv6CPE0wxgZJWxwkJoSrvwxUE+46JktSObUrhu4xQiqSCmBz8esQ=="], + "@scalar/api-client": ["@scalar/api-client@2.3.19", "", { "dependencies": { "@headlessui/tailwindcss": "^0.2.0", "@headlessui/vue": "^1.7.20", "@scalar/components": "0.13.47", "@scalar/draggable": "0.1.11", "@scalar/icons": "0.1.3", "@scalar/import": "0.3.13", "@scalar/oas-utils": "0.2.130", "@scalar/object-utils": "1.1.13", "@scalar/openapi-parser": "0.10.14", "@scalar/openapi-types": "0.2.0", "@scalar/postman-to-openapi": "0.2.3", "@scalar/snippetz": "0.2.19", "@scalar/themes": "0.9.86", "@scalar/types": "0.1.7", "@scalar/use-codemirror": "0.11.92", "@scalar/use-hooks": "0.1.40", "@scalar/use-toasts": "0.7.9", "@scalar/use-tooltip": "1.0.6", "@vueuse/core": "^10.10.0", "@vueuse/integrations": "^11.2.0", "focus-trap": "^7", "fuse.js": "^7.0.0", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "pretty-ms": "^8.0.0", "shell-quote": "^1.8.1", "type-fest": "^4.20.0", "vue": "^3.5.12", "vue-router": "^4.3.0", "whatwg-mimetype": "^4.0.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-1Scff4QL6UExxcmSYv5j1dktvQZTXbmDUJp99RqmUROEhneNWEeWaZe+GZWsda5mvDoe4vP9zZKaymkulBDKYQ=="], - "@scalar/api-client-react": ["@scalar/api-client-react@1.2.5", "", { "dependencies": { "@scalar/api-client": "2.3.5", "@scalar/types": "0.1.1", "vue": "^3.5.12" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-DpVDWb1rtNeU8ZqLX2Duos018q/++b3qIOYs7MzB2cXNg1802ZO7tPywXbYyizxLuy/dziI3UwGkjBuOKKXMGQ=="], + "@scalar/api-client-react": ["@scalar/api-client-react@1.2.19", "", { "dependencies": { "@scalar/api-client": "2.3.19", "@scalar/types": "0.1.7", "vue": "^3.5.12" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-zWUQvzAgOQ+oAbzswOlN42OJhwKku5RIIfBlukFMT31ceun3cp8e7+RT5DS6qwwQ2FROhSaqqsdM+o7VQ6pXkA=="], - "@scalar/code-highlight": ["@scalar/code-highlight@0.0.25", "", { "dependencies": { "hast-util-to-text": "^4.0.2", "highlight.js": "^11.9.0", "highlightjs-curl": "^1.3.0", "highlightjs-vue": "^1.0.0", "lowlight": "^3.1.0", "rehype-external-links": "^3.0.0", "rehype-format": "^5.0.0", "rehype-parse": "^9.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "remark-stringify": "^11.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-rmiXaAoL3Zl+OycIO1CMj8apaeAU/p41EmCpHTxInZiFVW0++iClce2fun1lK6qjTMZneR6UwE4qBKiUUVLCpg=="], + "@scalar/code-highlight": ["@scalar/code-highlight@0.0.27", "", { "dependencies": { "hast-util-to-text": "^4.0.2", "highlight.js": "^11.9.0", "highlightjs-curl": "^1.3.0", "highlightjs-vue": "^1.0.0", "lowlight": "^3.1.0", "rehype-external-links": "^3.0.0", "rehype-format": "^5.0.0", "rehype-parse": "^9.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "remark-stringify": "^11.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-A61FUxqD278L+iLtdbMl4+Pg72wtMrnAYft8v1FNY44uf6UfmM47eDVmzWrc7bSvDevg3ho5QA8cKiJBHXZHJA=="], - "@scalar/components": ["@scalar/components@0.13.37", "", { "dependencies": { "@floating-ui/utils": "^0.2.2", "@floating-ui/vue": "^1.0.2", "@headlessui/vue": "^1.7.20", "@scalar/code-highlight": "0.0.25", "@scalar/themes": "0.9.79", "@scalar/use-hooks": "0.1.33", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "cva": "1.0.0-beta.2", "nanoid": "^5.0.9", "pretty-bytes": "^6.1.1", "radix-vue": "^1.9.3", "tailwind-merge": "^2.5.5", "vue": "^3.5.12" } }, "sha512-bhJxg0I63nUH0qoZgb8nyHKCSzL8L9widP2WIYymIvXpCFLwCvF64Z0CAbihwgXxq0YblPvNM+g5N3dRtmXqdA=="], + "@scalar/components": ["@scalar/components@0.13.47", "", { "dependencies": { "@floating-ui/utils": "^0.2.2", "@floating-ui/vue": "^1.0.2", "@headlessui/vue": "^1.7.20", "@scalar/code-highlight": "0.0.27", "@scalar/themes": "0.9.86", "@scalar/use-hooks": "0.1.40", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "cva": "1.0.0-beta.2", "nanoid": "^5.1.5", "pretty-bytes": "^6.1.1", "radix-vue": "^1.9.3", "tailwind-merge": "^2.5.5", "vue": "^3.5.12" } }, "sha512-e88mKKsCEspd06bpPQPnhtEvCo/jjoFFOX9yUSV9sr0sWFZHi0ihq1zvnpLKpULyS+C5zzyoN/tGVhmaYpXgyg=="], "@scalar/draggable": ["@scalar/draggable@0.1.11", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-EQW9N1+mDORhsbjdtCI3XDvmUKsuKw1uf6r3kT1Mm2zQKT+rWwA0ChsAkEN6OG62C0YumMuXpH71h1seAWptxw=="], "@scalar/icons": ["@scalar/icons@0.1.3", "", { "dependencies": { "vue": "^3.5.12" } }, "sha512-Bl46u7WsJ7NYjW1Fva7SMvw9c/92pGBP8B68tvDc+QevQ04DVNxw6+ny1NU/PnLtpuu1rUpPdtSCAkV1OdQGZQ=="], - "@scalar/import": ["@scalar/import@0.3.2", "", { "dependencies": { "@scalar/oas-utils": "0.2.120", "@scalar/openapi-parser": "0.10.10", "yaml": "^2.4.5" } }, "sha512-de7IDZgEYOhhgaq8lFFbfiJ4Hx/ITIXvuLA8SfSl5UQKg+LxiOnnn/5PSKF+pnjWmUf7Kz/ds0eHOXpZv5iMkw=="], + "@scalar/import": ["@scalar/import@0.3.13", "", { "dependencies": { "@scalar/oas-utils": "0.2.130", "@scalar/openapi-parser": "0.10.14", "yaml": "^2.4.5" } }, "sha512-ooKyRxwtvMpxBnoLt9mSJF8er5rCR6RzGJaIMRCj7ViN776eY4mbLiYcXre/LO8XfLSHb1p7XbNDhfFyTHaUlw=="], - "@scalar/oas-utils": ["@scalar/oas-utils@0.2.120", "", { "dependencies": { "@hyperjump/json-schema": "^1.9.6", "@scalar/object-utils": "1.1.13", "@scalar/openapi-types": "0.1.9", "@scalar/themes": "0.9.79", "@scalar/types": "0.1.1", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.0.9", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-npu0uLClqqXVZfxMdKBWxkWCmONK0jKaUcfmVhGza9Jij5aJyvdfDw6vH/Hh+DghgECwAvQLQeIBZTxjr9ufzg=="], + "@scalar/oas-utils": ["@scalar/oas-utils@0.2.130", "", { "dependencies": { "@hyperjump/json-schema": "^1.9.6", "@scalar/object-utils": "1.1.13", "@scalar/openapi-types": "0.2.0", "@scalar/themes": "0.9.86", "@scalar/types": "0.1.7", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "^3.23.8" } }, "sha512-sVpdc3+3c/WiNrKEIwzJ+ml2ZQBjarMOTDJCM/IrvYhrJE0nHrdkzxlJgNPi++vJbVl0saYt8LhEItALv7NziA=="], "@scalar/object-utils": ["@scalar/object-utils@1.1.13", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1" } }, "sha512-311eTykIXgOtjCs4VTELj9UMT97jHTWc5qkGNoIzZ5nxjCcvOVe7kDQobIkE8dGT+ybOgHz5qly02Eu7nVHeZQ=="], - "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.10", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-6MSgvpNKu/anZy96dn8tXQZo1PuDCoeB4m2ZLLDS4vC2zaTnuNBvvQHx+gjwXNKWhTbIVy8bQpYBzlMAYnFNcQ=="], + "@scalar/openapi-parser": ["@scalar/openapi-parser@0.10.14", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-VXr979NMx6wZ+kpFKor2eyCJZOjyMwcBRc6c4Gc92ZMOC7ZNYqjwbw+Ubh2ELJyP5cWAjOFSrNwtylema0pw5w=="], "@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="], - "@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.1.43", "", { "dependencies": { "@scalar/oas-utils": "0.2.120", "@scalar/openapi-types": "0.1.9" } }, "sha512-gLOkYYPCTKYFBOwyBOKZDc0seZjntmwPTchJUr3oxGQmLB1Y5VBJ+8fXJCTp5TwKiiztjALJs79y9s7jXBdWMA=="], + "@scalar/postman-to-openapi": ["@scalar/postman-to-openapi@0.2.3", "", { "dependencies": { "@scalar/oas-utils": "0.2.130", "@scalar/openapi-types": "0.2.0" } }, "sha512-/I5QbDFy+Sh29EIEgub/ztI+1eNtHRn+mln726hR+uWOyVyaDk0FfNC0R4XOn8SsDNyu5eqPbXBb9vceTVG6jQ=="], - "@scalar/snippetz": ["@scalar/snippetz@0.2.16", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-xtIY4kvV619IF2uXg6fDw7emtXwuJeWzLunGAaUTIOwNRw5mGSKqJnLcSnIiSdH54YmN8D2CtdJRo2VxPP9/Wg=="], + "@scalar/snippetz": ["@scalar/snippetz@0.2.19", "", { "dependencies": { "stringify-object": "^5.0.0" } }, "sha512-fxC5mL3AZWiXAM21sMe1QU1/mu5KceN8ZmzFaP3xmdK26o/MkPKSLGVWW7w6OQkZi5hNloLHXXQiaI235qomEg=="], - "@scalar/themes": ["@scalar/themes@0.9.79", "", { "dependencies": { "@scalar/types": "0.1.1" } }, "sha512-zWiHCZAIjPGa8X9o/NORBPRMTMblLEz2+2RcfW9yIKNO/8H4Gz0rltiGGlJ6vX0o+qHwx7AdgfY+7njmWQR4ng=="], + "@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="], - "@scalar/types": ["@scalar/types@0.1.1", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-LlUX6AmOOGoRqOMoO835V2FezM1KiO5UlvQC3poT/s7oqD6ranqwRNFxyrPz/IxClPYR+SV1yBUSNKely4ZQhQ=="], + "@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="], - "@scalar/use-codemirror": ["@scalar/use-codemirror@0.11.82", "", { "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.8", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.10.7", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", "@lezer/common": "^1.2.3", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", "@replit/codemirror-css-color-picker": "^6.3.0", "@scalar/components": "0.13.37", "codemirror": "^6.0.0", "style-mod": "^4.1.2", "vue": "^3.5.12" } }, "sha512-zFECln7aWKRf6iJO9oovByD59EsrOMenNLfLhneH6L+K1CrBoHFVr4czSDlom1wlr3HPg3xwpZrukoAteHYILQ=="], + "@scalar/use-codemirror": ["@scalar/use-codemirror@0.11.92", "", { "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.8", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.10.7", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", "@lezer/common": "^1.2.3", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.2", "@replit/codemirror-css-color-picker": "^6.3.0", "@scalar/components": "0.13.47", "codemirror": "^6.0.0", "style-mod": "^4.1.2", "vue": "^3.5.12" } }, "sha512-WDd50xGLV+q1T36cKzmhqYP+TyHe4MOW0tIiu09ed9aMXGUk6kQgnf0J3rYPsGeNj9IkVG3znZ7oYT3QplLGVA=="], - "@scalar/use-hooks": ["@scalar/use-hooks@0.1.33", "", { "dependencies": { "@scalar/themes": "0.9.79", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "vue": "^3.5.12", "zod": "^3.23.8" } }, "sha512-ENm0bWwRdAWWF/S6TbE+fFx0vP2mgEpG5APqQBomm0a41/6L2HJ/TN+9ajAvrJXGi0ULWuxihNS4Jue6tpEssA=="], + "@scalar/use-hooks": ["@scalar/use-hooks@0.1.40", "", { "dependencies": { "@scalar/themes": "0.9.86", "@scalar/use-toasts": "0.7.9", "@vueuse/core": "^10.10.0", "vue": "^3.5.12", "zod": "^3.23.8" } }, "sha512-z8qtgIcW9Z3PCrP2cbKG+D2EVhpNgl1N0ucGtDg5SMl/fvCyXNfqB9j+u3ygxkouatfQ9zRZuhxreNMkW9/H5g=="], "@scalar/use-toasts": ["@scalar/use-toasts@0.7.9", "", { "dependencies": { "nanoid": "^5.0.9", "vue": "^3.5.12", "vue-sonner": "^1.0.3" } }, "sha512-EcUDJY8VozLS9sfoQKvvipStQJ9RuH/nKOzf0BBr+mZDmumi1WFZ1iIJnHVXIN3iSLcSAr5ej6rOqa6jIv4bCQ=="], @@ -1315,8 +1327,6 @@ "@unhead/schema": ["@unhead/schema@1.11.14", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-V9W9u5tF1/+TiLqxu+Qvh1ShoMDkPEwHoEo4DKdDG6ko7YlbzFfDxV6el9JwCren45U/4Vy/4Xi7j8OH02wsiA=="], - "@upstash/redis": ["@upstash/redis@1.34.3", "", { "dependencies": { "crypto-js": "^4.2.0" } }, "sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ=="], - "@vercel/build-utils": ["@vercel/build-utils@9.0.1", "", {}, "sha512-pG/izEqA0AGyqQj6QBfGoTOKU9FPG18sYw9qpncEq00uA+J4Ly4e8ssNbENsXtnXqkMjeoS3c5ncR3jT0bOyiA=="], "@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="], @@ -1385,7 +1395,7 @@ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], @@ -1393,7 +1403,7 @@ "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], - "ai": ["ai@4.1.47", "", { "dependencies": { "@ai-sdk/provider": "1.0.9", "@ai-sdk/provider-utils": "2.1.10", "@ai-sdk/react": "1.1.19", "@ai-sdk/ui-utils": "1.1.16", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.0.0" }, "optionalPeers": ["react", "zod"] }, "sha512-9UZ8Mkv1HlprCJfQ0Kq+rgKbfkrkDtJjslr1WOHBRzvC70jDJJYp8r0Qq4vlF7zs9VBkGyy8Rhm5zQJJIUjvgw=="], + "ai": ["ai@4.3.0", "", { "dependencies": { "@ai-sdk/provider": "1.1.0", "@ai-sdk/provider-utils": "2.2.4", "@ai-sdk/react": "1.2.6", "@ai-sdk/ui-utils": "1.2.5", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-PxyQYKhWaU3LiZEpeKRaekVonZIbWdKAwgnqm0CSAxy1MFufmYEC5SM5Mc9uiK2DoHcbAL3d1jyaQ2fSDAJL8w=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -1445,7 +1455,7 @@ "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], - "axios": ["axios@1.7.9", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw=="], + "axios": ["axios@1.8.4", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -1505,8 +1515,6 @@ "caniuse-lite": ["caniuse-lite@1.0.30001668", "", {}, "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw=="], - "capnp-ts": ["capnp-ts@0.7.0", "", { "dependencies": { "debug": "^4.3.1", "tslib": "^2.2.0" } }, "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g=="], - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], @@ -1559,8 +1567,6 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], - "configstore": ["configstore@5.0.1", "", { "dependencies": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" } }, "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA=="], "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], @@ -1585,8 +1591,6 @@ "cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], - "crypto-js": ["crypto-js@4.2.0", "", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="], - "crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="], "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="], @@ -1705,7 +1709,7 @@ "es6-weak-map": ["es6-weak-map@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.46", "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.1" } }, "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA=="], - "esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "esbuild-android-64": ["esbuild-android-64@0.15.18", "", { "os": "android", "cpu": "x64" }, "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA=="], @@ -1753,7 +1757,7 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], @@ -1779,6 +1783,8 @@ "express": ["express@5.0.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.0.1", "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "^1.2.1", "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "^2.0.0", "fresh": "2.0.0", "http-errors": "2.0.0", "merge-descriptors": "^2.0.0", "methods": "~1.1.2", "mime-types": "^3.0.0", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "~1.3.3", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "router": "^2.0.0", "safe-buffer": "5.2.1", "send": "^1.1.0", "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ=="], + "exsolve": ["exsolve@1.0.4", "", {}, "sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2281,13 +2287,13 @@ "mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], - "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], "mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="], + "miniflare": ["miniflare@4.20250409.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250409.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Hu02dYZvFR+MyrI57O6rSrOUTofcO9EIvcodgq2SAHzAeWSJw2E0oq9lylOrcckFwPMcwxUAb/cQN1LIoCyySw=="], "minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="], @@ -2303,8 +2309,6 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], - "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -2315,11 +2319,11 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g=="], + "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "next": ["next@14.2.25", "", { "dependencies": { "@next/env": "14.2.25", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.25", "@next/swc-darwin-x64": "14.2.25", "@next/swc-linux-arm64-gnu": "14.2.25", "@next/swc-linux-arm64-musl": "14.2.25", "@next/swc-linux-x64-gnu": "14.2.25", "@next/swc-linux-x64-musl": "14.2.25", "@next/swc-win32-arm64-msvc": "14.2.25", "@next/swc-win32-ia32-msvc": "14.2.25", "@next/swc-win32-x64-msvc": "14.2.25" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-N5M7xMc4wSb4IkPvEV5X2BRRXUmhVHNyaXwEM86+voXthSZz8ZiRyQW4p9mwAoAPIm6OzuVZtn7idgEJeAJN3Q=="], + "next": ["next@14.2.26", "", { "dependencies": { "@next/env": "14.2.26", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.26", "@next/swc-darwin-x64": "14.2.26", "@next/swc-linux-arm64-gnu": "14.2.26", "@next/swc-linux-arm64-musl": "14.2.26", "@next/swc-linux-x64-gnu": "14.2.26", "@next/swc-linux-x64-musl": "14.2.26", "@next/swc-win32-arm64-msvc": "14.2.26", "@next/swc-win32-ia32-msvc": "14.2.26", "@next/swc-win32-x64-msvc": "14.2.26" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw=="], "next-themes": ["next-themes@0.2.1", "", { "peerDependencies": { "next": "*", "react": "*", "react-dom": "*" } }, "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A=="], @@ -2357,7 +2361,7 @@ "obliterator": ["obliterator@1.6.1", "", {}, "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig=="], - "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -2369,7 +2373,7 @@ "oniguruma-to-es": ["oniguruma-to-es@4.1.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "oniguruma-parser": "^0.5.4", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA=="], - "openapi-fetch": ["openapi-fetch@0.13.4", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-JHX7UYjLEiHuQGCPxa3CCCIqe/nc4bTIF9c4UYVC8BegAbWoS3g4gJxKX5XcG7UtYQs2060kY6DH64KkvNZahg=="], + "openapi-fetch": ["openapi-fetch@0.13.5", "", { "dependencies": { "openapi-typescript-helpers": "^0.0.15" } }, "sha512-AQK8T9GSKFREFlN1DBXTYsLjs7YV2tZcJ7zUWxbjMoQmj8dDSFRrzhLCbHPZWA1TMV3vACqfCxLEZcwf2wxV6Q=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -2393,8 +2397,6 @@ "p-map": ["p-map@7.0.2", "", {}, "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q=="], - "p-memoize": ["p-memoize@7.1.1", "", { "dependencies": { "mimic-fn": "^4.0.0", "type-fest": "^3.0.0" } }, "sha512-DZ/bONJILHkQ721hSr/E9wMz5Am/OTJ9P6LhLFo2Tu+jL8044tgc9LwHO8g4PiaYePnlVVRAJcKmgy8J9MVFrA=="], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], "package-json": ["package-json@6.5.0", "", { "dependencies": { "got": "^9.6.0", "registry-auth-token": "^4.0.0", "registry-url": "^5.0.0", "semver": "^6.2.0" } }, "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ=="], @@ -2417,6 +2419,8 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -2435,7 +2439,7 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pcre-to-regexp": ["pcre-to-regexp@1.1.0", "", {}, "sha512-KF9XxmUQJ2DIlMj3TqNqY1AWvyvTuIuq11CuuekxyaYMiFuMKGgQrePYMX5bXKLhLG3sDI4CsGAYHPaT7VV7+g=="], @@ -2449,8 +2453,6 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], - "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "playwright": ["playwright@1.51.1", "", { "dependencies": { "playwright-core": "1.51.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw=="], "playwright-core": ["playwright-core@1.51.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw=="], @@ -2513,13 +2515,13 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], "react-aria": ["react-aria@3.37.0", "", { "dependencies": { "@internationalized/string": "^3.2.5", "@react-aria/breadcrumbs": "^3.5.20", "@react-aria/button": "^3.11.1", "@react-aria/calendar": "^3.7.0", "@react-aria/checkbox": "^3.15.1", "@react-aria/color": "^3.0.3", "@react-aria/combobox": "^3.11.1", "@react-aria/datepicker": "^3.13.0", "@react-aria/dialog": "^3.5.21", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/gridlist": "^3.10.1", "@react-aria/i18n": "^3.12.5", "@react-aria/interactions": "^3.23.0", "@react-aria/label": "^3.7.14", "@react-aria/link": "^3.7.8", "@react-aria/listbox": "^3.14.0", "@react-aria/menu": "^3.17.0", "@react-aria/meter": "^3.4.19", "@react-aria/numberfield": "^3.11.10", "@react-aria/overlays": "^3.25.0", "@react-aria/progress": "^3.4.19", "@react-aria/radio": "^3.10.11", "@react-aria/searchfield": "^3.8.0", "@react-aria/select": "^3.15.1", "@react-aria/selection": "^3.22.0", "@react-aria/separator": "^3.4.5", "@react-aria/slider": "^3.7.15", "@react-aria/ssr": "^3.9.7", "@react-aria/switch": "^3.6.11", "@react-aria/table": "^3.16.1", "@react-aria/tabs": "^3.9.9", "@react-aria/tag": "^3.4.9", "@react-aria/textfield": "^3.16.0", "@react-aria/tooltip": "^3.7.11", "@react-aria/utils": "^3.27.0", "@react-aria/visually-hidden": "^3.8.19", "@react-types/shared": "^3.27.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-u3WUEMTcbQFaoHauHO3KhPaBYzEv1o42EdPcLAs05GBw9Q6Axlqwo73UFgMrsc2ElwLAZ4EKpSdWHLo1R5gfiw=="], "react-aria-components": ["react-aria-components@1.6.0", "", { "dependencies": { "@internationalized/date": "^3.7.0", "@internationalized/string": "^3.2.5", "@react-aria/autocomplete": "3.0.0-alpha.37", "@react-aria/collections": "3.0.0-alpha.7", "@react-aria/color": "^3.0.3", "@react-aria/disclosure": "^3.0.1", "@react-aria/dnd": "^3.8.1", "@react-aria/focus": "^3.19.1", "@react-aria/interactions": "^3.23.0", "@react-aria/live-announcer": "^3.4.1", "@react-aria/menu": "^3.17.0", "@react-aria/toolbar": "3.0.0-beta.12", "@react-aria/tree": "3.0.0-beta.3", "@react-aria/utils": "^3.27.0", "@react-aria/virtualizer": "^4.1.1", "@react-stately/autocomplete": "3.0.0-alpha.0", "@react-stately/color": "^3.8.2", "@react-stately/disclosure": "^3.0.1", "@react-stately/layout": "^4.1.1", "@react-stately/menu": "^3.9.1", "@react-stately/selection": "^3.19.0", "@react-stately/table": "^3.13.1", "@react-stately/utils": "^3.10.5", "@react-stately/virtualizer": "^4.2.1", "@react-types/color": "^3.0.2", "@react-types/form": "^3.7.9", "@react-types/grid": "^3.2.11", "@react-types/shared": "^3.27.0", "@react-types/table": "^3.10.4", "@swc/helpers": "^0.5.0", "client-only": "^0.0.1", "react-aria": "^3.37.0", "react-stately": "^3.35.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-YfG9PUE7XrXtDDAqT4pLTGyYQaiHHTBFdAK/wNgGsypVnQSdzmyYlV3Ty8aHlZJI6hP9RWkbywvosXkU7KcPHg=="], - "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-hotkeys-hook": ["react-hotkeys-hook@4.5.1", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg=="], @@ -2593,12 +2595,6 @@ "rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="], - "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], - - "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], - - "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], - "router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -2607,7 +2603,7 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], @@ -2655,8 +2651,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], @@ -2787,25 +2781,27 @@ "ts-toolbelt": ["ts-toolbelt@6.15.5", "", {}, "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="], + "ts-tqdm": ["ts-tqdm@0.8.6", "", {}, "sha512-3X3M1PZcHtgQbnwizL+xU8CAgbYbeLHrrDwL9xxcZZrV5J+e7loJm1XrXozHjSkl44J0Zg0SgA8rXbh83kCkcQ=="], + "tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="], - "turbo": ["turbo@2.4.4", "", { "optionalDependencies": { "turbo-darwin-64": "2.4.4", "turbo-darwin-arm64": "2.4.4", "turbo-linux-64": "2.4.4", "turbo-linux-arm64": "2.4.4", "turbo-windows-64": "2.4.4", "turbo-windows-arm64": "2.4.4" }, "bin": { "turbo": "bin/turbo" } }, "sha512-N9FDOVaY3yz0YCOhYIgOGYad7+m2ptvinXygw27WPLQvcZDl3+0Sa77KGVlLSiuPDChOUEnTKE9VJwLSi9BPGQ=="], + "turbo": ["turbo@2.5.0", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.0", "turbo-darwin-arm64": "2.5.0", "turbo-linux-64": "2.5.0", "turbo-linux-arm64": "2.5.0", "turbo-windows-64": "2.5.0", "turbo-windows-arm64": "2.5.0" }, "bin": { "turbo": "bin/turbo" } }, "sha512-PvSRruOsitjy6qdqwIIyolv99+fEn57gP6gn4zhsHTEcCYgXPhv6BAxzAjleS8XKpo+Y582vTTA9nuqYDmbRuA=="], - "turbo-darwin-64": ["turbo-darwin-64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-5kPvRkLAfmWI0MH96D+/THnDMGXlFNmjeqNRj5grLKiry+M9pKj3pRuScddAXPdlxjO5Ptz06UNaOQrrYGTx1g=="], + "turbo-darwin-64": ["turbo-darwin-64@2.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fP1hhI9zY8hv0idym3hAaXdPi80TLovmGmgZFocVAykFtOxF+GlfIgM/l4iLAV9ObIO4SUXPVWHeBZQQ+Hpjag=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/gtHPqbGQXDFhrmy+Q/MFW2HUTUlThJ97WLLSe4bxkDrKHecDYhAjbZ4rN3MM93RV9STQb3Tqy4pZBtsd4DfCw=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p9sYq7kXH7qeJwIQE86cOWv/xNqvow846l6c/qWc26Ib1ci5W7V0sI5thsrP3eH+VA0d+SHalTKg5SQXgNQBWA=="], - "turbo-linux-64": ["turbo-linux-64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-SR0gri4k0bda56hw5u9VgDXLKb1Q+jrw4lM7WAhnNdXvVoep4d6LmnzgMHQQR12Wxl3KyWPbkz9d1whL6NTm2Q=="], + "turbo-linux-64": ["turbo-linux-64@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1iEln2GWiF3iPPPS1HQJT6ZCFXynJPd89gs9SkggH2EJsj3eRUSVMmMC8y6d7bBbhBFsiGGazwFIYrI12zs6uQ=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-COXXwzRd3vslQIfJhXUklgEqlwq35uFUZ7hnN+AUyXx7hUOLIiD5NblL+ETrHnhY4TzWszrbwUMfe2BYWtaPQg=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-bKBcbvuQHmsX116KcxHJuAcppiiBOfivOObh2O5aXNER6mce7YDDQJy00xQQNp1DhEfcSV2uOsvb3O3nN2cbcA=="], - "turbo-windows-64": ["turbo-windows-64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-PV9rYNouGz4Ff3fd6sIfQy5L7HT9a4fcZoEv8PKRavU9O75G7PoDtm8scpHU10QnK0QQNLbE9qNxOAeRvF0fJg=="], + "turbo-windows-64": ["turbo-windows-64@2.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-9BCo8oQ7BO7J0K913Czbc3tw8QwLqn2nTe4E47k6aVYkM12ASTScweXPTuaPFP5iYXAT6z5Dsniw704Ixa5eGg=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-403sqp9t5sx6YGEC32IfZTVWkRAixOQomGYB8kEc6ZD+//LirSxzeCHCnM8EmSXw7l57U1G+Fb0kxgTcKPU/Lg=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OUHCV+ueXa3UzfZ4co/ueIHgeq9B2K48pZwIxKSm5VaLVuv8M13MhM7unukW09g++dpdrrE1w4IOVgxKZ0/exg=="], "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], - "type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], + "type-fest": ["type-fest@4.40.0", "", {}, "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw=="], "type-is": ["type-is@2.0.0", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw=="], @@ -2821,7 +2817,7 @@ "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="], + "unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -2911,9 +2907,9 @@ "widest-line": ["widest-line@3.1.0", "", { "dependencies": { "string-width": "^4.0.0" } }, "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg=="], - "workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="], + "workerd": ["workerd@1.20250409.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250409.0", "@cloudflare/workerd-darwin-arm64": "1.20250409.0", "@cloudflare/workerd-linux-64": "1.20250409.0", "@cloudflare/workerd-linux-arm64": "1.20250409.0", "@cloudflare/workerd-windows-64": "1.20250409.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-hqjX9swiHvrkOI3jlH9lrZsZRRv9lddUwcMe8Ua76jnyQz+brybWznNjHu8U5oswwcrFwvky1A4CcLjcLY31gQ=="], - "wrangler": ["wrangler@3.112.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250214.2", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.1", "workerd": "1.20250214.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250214.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-PNQWGze3ODlWwG33LPr8kNhbht3eB3L9fogv+fapk2fjaqj0kNweRapkwmvtz46ojcqWzsxmTe4nOC0hIVUfPA=="], + "wrangler": ["wrangler@4.10.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.3.1", "blake3-wasm": "2.1.5", "esbuild": "0.24.2", "miniflare": "4.20250409.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.15", "workerd": "1.20250409.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250409.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-fTE4hZ79msEUt8+HEjl/8Q72haCyzPLu4PgrU3L81ysmjrMEdiYfUPqnvCkBUVtJvrDNdctTEimkufT1Y0ipNg=="], "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -2945,13 +2941,13 @@ "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], - "youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], + "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], - "zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="], + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - "zod-to-json-schema": ["zod-to-json-schema@3.24.3", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A=="], + "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="], @@ -3551,13 +3547,13 @@ "@changesets/parse/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "@cloudflare/next-on-pages/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="], - "@cloudflare/next-on-pages/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "@cloudflare/next-on-pages/esbuild": ["esbuild@0.15.18", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="], - "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="], + "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20250214.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-t+lT4p2lbOcKv4PS3sx1F/wcDAlbEYZCO2VooLp4H7JErWWYIi9yjD3UillC3CGOpiBahVg5nrPCoFltZf6UlA=="], + + "@cloudflare/next-on-pages/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], @@ -3653,7 +3649,7 @@ "@node-minify/core/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "@opennextjs/aws/esbuild": ["esbuild@0.19.2", "", { "optionalDependencies": { "@esbuild/android-arm": "0.19.2", "@esbuild/android-arm64": "0.19.2", "@esbuild/android-x64": "0.19.2", "@esbuild/darwin-arm64": "0.19.2", "@esbuild/darwin-x64": "0.19.2", "@esbuild/freebsd-arm64": "0.19.2", "@esbuild/freebsd-x64": "0.19.2", "@esbuild/linux-arm": "0.19.2", "@esbuild/linux-arm64": "0.19.2", "@esbuild/linux-ia32": "0.19.2", "@esbuild/linux-loong64": "0.19.2", "@esbuild/linux-mips64el": "0.19.2", "@esbuild/linux-ppc64": "0.19.2", "@esbuild/linux-riscv64": "0.19.2", "@esbuild/linux-s390x": "0.19.2", "@esbuild/linux-x64": "0.19.2", "@esbuild/netbsd-x64": "0.19.2", "@esbuild/openbsd-x64": "0.19.2", "@esbuild/sunos-x64": "0.19.2", "@esbuild/win32-arm64": "0.19.2", "@esbuild/win32-ia32": "0.19.2", "@esbuild/win32-x64": "0.19.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg=="], + "@opennextjs/aws/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="], "@radix-ui/react-collection/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -3667,18 +3663,102 @@ "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], + "@radix-ui/react-dropdown-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@radix-ui/react-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-menu/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-menu/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + + "@radix-ui/react-menu/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA=="], + + "@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.4", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.4", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA=="], + + "@radix-ui/react-menu/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw=="], + + "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], + + "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-menu/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-menu/react-remove-scroll": ["react-remove-scroll@2.6.3", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ=="], + "@radix-ui/react-navigation-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], "@radix-ui/react-navigation-menu/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.1", "", { "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ=="], + "@radix-ui/react-popover/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A=="], + "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.0", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-tooltip/@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], + + "@radix-ui/react-tooltip/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], + + "@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "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-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="], + + "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.1", "", { "dependencies": { "@radix-ui/react-slot": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg=="], "@react-aria/focus/clsx": ["clsx@2.0.0", "", {}, "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="], @@ -3687,14 +3767,20 @@ "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], - "@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], + "@scalar/api-client/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], - "@scalar/code-highlight/remark-gfm": ["remark-gfm@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA=="], + "@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="], - "@scalar/oas-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], + "@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], "@scalar/object-utils/flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], + "@scalar/postman-to-openapi/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="], + + "@scalar/use-toasts/nanoid": ["nanoid@5.1.2", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g=="], + "@shikijs/core/hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "@smithy/abort-controller/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -3885,8 +3971,6 @@ "@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - "@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "@vercel/nft/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@vercel/nft/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -3943,8 +4027,6 @@ "cacheable-request/lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "capnp-ts/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "codemirror/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], "codemirror/@codemirror/commands": ["@codemirror/commands@6.7.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-+cduIZ2KbesDhbykV02K25A5xIVrquSPz4UxxYBemRlAT2aW8dhwUgLDwej7q/RJUHKk4nALYcR1puecDvbdqw=="], @@ -3995,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.2.3", "", { "dependencies": { "@next/env": "15.2.3", "@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.2.3", "@next/swc-darwin-x64": "15.2.3", "@next/swc-linux-arm64-gnu": "15.2.3", "@next/swc-linux-arm64-musl": "15.2.3", "@next/swc-linux-x64-gnu": "15.2.3", "@next/swc-linux-x64-musl": "15.2.3", "@next/swc-win32-arm64-msvc": "15.2.3", "@next/swc-win32-x64-msvc": "15.2.3", "sharp": "^0.33.5" }, "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-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w=="], + "gitbook-v2/next": ["next@15.3.2", "", { "dependencies": { "@next/env": "15.3.2", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.2", "@next/swc-darwin-x64": "15.3.2", "@next/swc-linux-arm64-gnu": "15.3.2", "@next/swc-linux-arm64-musl": "15.3.2", "@next/swc-linux-x64-gnu": "15.3.2", "@next/swc-linux-x64-musl": "15.3.2", "@next/swc-win32-arm64-msvc": "15.3.2", "@next/swc-win32-x64-msvc": "15.3.2", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ=="], "global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="], @@ -4033,8 +4115,6 @@ "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "mdast-util-gfm/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], "mdast-util-gfm-footnote/mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ=="], @@ -4053,8 +4133,6 @@ "micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], @@ -4065,18 +4143,12 @@ "minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="], - "mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - - "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "normalize-package-data/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "p-filter/p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], "package-json/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4085,8 +4157,6 @@ "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], - "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], @@ -4125,12 +4195,6 @@ "rimraf/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - - "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], - - "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], - "router/is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], @@ -4169,10 +4233,10 @@ "terminal-link/supports-hyperlinks": ["supports-hyperlinks@2.3.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA=="], - "terser/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "ts-node/acorn": ["acorn@8.12.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg=="], + "ts-node/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], @@ -4187,6 +4251,8 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="], "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="], @@ -4617,11 +4683,13 @@ "@cloudflare/next-on-pages/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="], - "@cloudflare/next-on-pages/miniflare/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "@cloudflare/next-on-pages/miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], - "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="], + "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="], - "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], + + "@cloudflare/next-on-pages/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], "@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="], @@ -4653,54 +4721,98 @@ "@node-minify/core/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "@opennextjs/aws/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.19.2", "", { "os": "android", "cpu": "arm" }, "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q=="], + "@opennextjs/aws/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], + + "@opennextjs/aws/esbuild/@esbuild/android-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-x64": ["@esbuild/win32-x64@0.19.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw=="], + "@opennextjs/aws/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="], + + "@opennextjs/aws/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="], + + "@opennextjs/aws/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-dismissable-layer/@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-menu/@radix-ui/react-popper/@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@radix-ui/react-menu/@radix-ui/react-portal/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-menu/react-remove-scroll/react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "@radix-ui/react-menu/react-remove-scroll/react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "@radix-ui/react-menu/react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@radix-ui/react-menu/react-remove-scroll/use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "@radix-ui/react-menu/react-remove-scroll/use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], + "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], + + "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="], "@scalar/api-client/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="], @@ -4863,26 +4975,28 @@ "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.2.3", "", {}, "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw=="], + "gitbook-v2/next/@next/env": ["@next/env@15.3.2", "", {}, "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "gitbook-v2/next/sharp": ["sharp@0.34.1", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.7.1" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.1", "@img/sharp-darwin-x64": "0.34.1", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.1", "@img/sharp-linux-arm64": "0.34.1", "@img/sharp-linux-s390x": "0.34.1", "@img/sharp-linux-x64": "0.34.1", "@img/sharp-linuxmusl-arm64": "0.34.1", "@img/sharp-linuxmusl-x64": "0.34.1", "@img/sharp-wasm32": "0.34.1", "@img/sharp-win32-ia32": "0.34.1", "@img/sharp-win32-x64": "0.34.1" } }, "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg=="], + "gitbook-v2/next/styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "globby/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -5067,17 +5181,15 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="], - - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="], - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="], - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="], - "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="], - "@cloudflare/next-on-pages/miniflare/youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="], "@node-minify/core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5099,6 +5211,46 @@ "gitbook-v2/next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + "gitbook-v2/next/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A=="], + + "gitbook-v2/next/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], + + "gitbook-v2/next/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], + + "gitbook-v2/next/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA=="], + + "gitbook-v2/next/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ=="], + + "gitbook-v2/next/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA=="], + + "gitbook-v2/next/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA=="], + + "gitbook-v2/next/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ=="], + + "gitbook-v2/next/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg=="], + + "gitbook-v2/next/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.1", "", { "dependencies": { "@emnapi/runtime": "^1.4.0" }, "cpu": "none" }, "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg=="], + + "gitbook-v2/next/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw=="], + + "gitbook-v2/next/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw=="], + + "gitbook-v2/next/sharp/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -5141,6 +5293,8 @@ "@vercel/nft/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "gitbook-v2/next/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.4.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-64WYIf4UYcdLnbKn/umDlNjQDSS8AgZrI/R9+x5ilkUVFxXcA1Ebl+gQLc/6mERA4407Xof0R7wEyEuj091CVw=="], + "@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], "@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], @@ -5148,5 +5302,7 @@ "@aws-sdk/middleware-sdk-sqs/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], "@aws-sdk/middleware-sdk-sqs/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], + + "gitbook-v2/next/sharp/@img/sharp-wasm32/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], } } diff --git a/package.json b/package.json index b4d134bad..5398e52f9 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/cli": "^2.27.12", - "turbo": "^2.4.4", + "turbo": "^2.5.0", "vercel": "^39.3.0" }, - "packageManager": "bun@1.2.5", + "packageManager": "bun@1.2.11", "overrides": { "@codemirror/state": "6.4.1", - "react": "18.3.1", - "react-dom": "18.3.1", - "@gitbook/api": "0.106.0" + "@gitbook/api": "^0.115.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" }, "private": true, "scripts": { diff --git a/packages/cache-do/package.json b/packages/cache-do/package.json index d41203a0b..bcdae42ef 100644 --- a/packages/cache-do/package.json +++ b/packages/cache-do/package.json @@ -21,10 +21,10 @@ }, "devDependencies": { "typescript": "^5.5.3", - "wrangler": "^3.112.0" + "wrangler": "^4.10.0" }, "scripts": { - "generate": "wrangler types --experimental-include-runtime", + "generate": "wrangler types", "build": "tsc", "typecheck": "tsc --noEmit", "dev": "tsc -w", diff --git a/packages/cache-do/tsconfig.json b/packages/cache-do/tsconfig.json index 09c216749..94283aa2e 100644 --- a/packages/cache-do/tsconfig.json +++ b/packages/cache-do/tsconfig.json @@ -14,7 +14,7 @@ "resolveJsonModule": true, "isolatedModules": true, "incremental": true, - "types": ["./.wrangler/types/runtime.d.ts"] + "types": ["./worker-configuration.d.ts"] }, "include": ["src/**/*.ts"], "exclude": ["node_modules"] diff --git a/packages/cache-do/turbo.json b/packages/cache-do/turbo.json new file mode 100644 index 000000000..bf952e187 --- /dev/null +++ b/packages/cache-do/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "generate": { + "outputs": ["worker-configuration.d.ts"] + } + } +} diff --git a/packages/cache-tags/CHANGELOG.md b/packages/cache-tags/CHANGELOG.md index 46afb6039..a5589ab56 100644 --- a/packages/cache-tags/CHANGELOG.md +++ b/packages/cache-tags/CHANGELOG.md @@ -1,5 +1,17 @@ # @gitbook/cache-tags +## 0.3.1 + +### Patch Changes + +- 77397ca: Fix version of @gitbook/api referenced in package.json + +## 0.3.0 + +### Minor Changes + +- 116575c: Improve typing of getComputedContentSourceCacheTags to match latest API specification + ## 0.2.0 ### Minor Changes diff --git a/packages/cache-tags/package.json b/packages/cache-tags/package.json index b55402545..8850dafbd 100644 --- a/packages/cache-tags/package.json +++ b/packages/cache-tags/package.json @@ -8,9 +8,9 @@ "default": "./dist/index.js" } }, - "version": "0.2.0", + "version": "0.3.1", "dependencies": { - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "assert-never": "^1.2.1" }, "devDependencies": { diff --git a/packages/colors/CHANGELOG.md b/packages/colors/CHANGELOG.md index d73704be2..1695a6c97 100644 --- a/packages/colors/CHANGELOG.md +++ b/packages/colors/CHANGELOG.md @@ -1,5 +1,19 @@ # @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 + +- cdffd7c: Desaturate text colors by decreasing chroma for the last steps of the color scale + ## 0.3.1 ### Patch Changes diff --git a/packages/colors/package.json b/packages/colors/package.json index af67871cb..16f54d1ff 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -8,7 +8,7 @@ "default": "./dist/index.js" } }, - "version": "0.3.1", + "version": "0.3.3", "devDependencies": { "typescript": "^5.5.3" }, diff --git a/packages/colors/src/transformations.ts b/packages/colors/src/transformations.ts index ab0e25c31..8234a460a 100644 --- a/packages/colors/src/transformations.ts +++ b/packages/colors/src/transformations.ts @@ -214,13 +214,31 @@ export function colorScale( const targetL = foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]); - if (index === 8 && !mix && Math.abs(baseColor.L - targetL) < 0.2) { + if ( + index === 8 && + !mix && + (darkMode ? targetL - baseColor.L < 0.2 : baseColor.L - targetL < 0.2) + ) { // Original colour is close enough to target, so let's use the original colour as step 9. result.push(hex); continue; } - const chromaRatio = index < 8 ? index * 0.05 : 1; + const chromaRatio = (() => { + switch (index) { + // Step 9 and 10 have max chroma, meaning they are fully saturated. + case 8: + case 9: + return 1; + // Step 11 and 12 have a reduced chroma + case 10: + return 0.4; + case 11: + return 0.1; + default: + return index * 0.05; + } + })(); const shade = { L: targetL, // Blend lightness diff --git a/packages/gitbook-v2/.gitignore b/packages/gitbook-v2/.gitignore index 84254823d..a23d5b756 100644 --- a/packages/gitbook-v2/.gitignore +++ b/packages/gitbook-v2/.gitignore @@ -6,6 +6,7 @@ # cloudflare .open-next +.wrangler # Symbolic links public diff --git a/packages/gitbook-v2/CHANGELOG.md b/packages/gitbook-v2/CHANGELOG.md index b79ef4dd6..8a4a4a028 100644 --- a/packages/gitbook-v2/CHANGELOG.md +++ b/packages/gitbook-v2/CHANGELOG.md @@ -1,5 +1,39 @@ # 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 + +- Updated dependencies [77397ca] + - @gitbook/cache-tags@0.3.1 + +## 0.2.4 + +### Patch Changes + +- 4234289: Fix incoming URL for requests that were proxied +- Updated dependencies [116575c] + - @gitbook/cache-tags@0.3.0 + ## 0.2.3 ### Patch Changes diff --git a/packages/gitbook-v2/next.config.mjs b/packages/gitbook-v2/next.config.mjs index 659c9a536..d915cf312 100644 --- a/packages/gitbook-v2/next.config.mjs +++ b/packages/gitbook-v2/next.config.mjs @@ -7,8 +7,6 @@ const nextConfig = { experimental: { // This is needed to throw "forbidden" when the api token expired during revalidation authInterrupts: true, - - // This is needed to use 'use cache' useCache: true, // Content is fully static, we can cache it in the session memory cache for a long time @@ -33,7 +31,9 @@ const nextConfig = { GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX, GITBOOK_SECRET: process.env.GITBOOK_SECRET, GITBOOK_IMAGE_RESIZE_SIGNING_KEY: process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY, + GITBOOK_IMAGE_RESIZE_MODE: process.env.GITBOOK_IMAGE_RESIZE_MODE, GITBOOK_FONTS_URL: process.env.GITBOOK_FONTS_URL, + GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME, // Next.js envs NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, @@ -53,6 +53,24 @@ const nextConfig = { }, ], }, + + async headers() { + return [ + { + source: '/~gitbook/static/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + { + key: 'Access-Control-Allow-Origin', + value: '*', + }, + ], + }, + ]; + }, }; export default nextConfig; diff --git a/packages/gitbook-v2/open-next.config.ts b/packages/gitbook-v2/open-next.config.ts index 5b590c3aa..d35c9ef03 100644 --- a/packages/gitbook-v2/open-next.config.ts +++ b/packages/gitbook-v2/open-next.config.ts @@ -1,10 +1,26 @@ import { defineCloudflareConfig } from '@opennextjs/cloudflare'; -import d1TagCache from '@opennextjs/cloudflare/d1-tag-cache'; -import kvIncrementalCache from '@opennextjs/cloudflare/kv-cache'; -import memoryQueue from '@opennextjs/cloudflare/memory-queue'; +import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache'; +import { + softTagFilter, + withFilter, +} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter'; export default defineCloudflareConfig({ - incrementalCache: kvIncrementalCache, - queue: memoryQueue, - tagCache: d1TagCache, + incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default), + tagCache: withFilter({ + tagCache: doShardedTagCache({ + baseShardSize: 12, + regionalCache: true, + shardReplication: { + numberOfSoftReplicas: 2, + numberOfHardReplicas: 1, + }, + }), + // We don't use `revalidatePath`, so we filter out soft tags + filterFn: softTagFilter, + }), + queue: () => import('./openNext/queue').then((m) => m.default), + + // Performance improvements as we don't use PPR + enableCacheInterception: true, }); diff --git a/packages/gitbook-v2/openNext/incrementalCache.ts b/packages/gitbook-v2/openNext/incrementalCache.ts new file mode 100644 index 000000000..d4a662af0 --- /dev/null +++ b/packages/gitbook-v2/openNext/incrementalCache.ts @@ -0,0 +1,186 @@ +import { createHash } from 'node:crypto'; + +import { trace } from '@/lib/tracing'; +import type { + CacheEntryType, + CacheValue, + IncrementalCache, + WithLastModified, +} from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; + +export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET'; +export const DEFAULT_PREFIX = 'incremental-cache'; + +export type KeyOptions = { + cacheType?: CacheEntryType; +}; + +/** + * + * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace + * the cache operations. It also integrates both R2 and Cache API in a single class. + * Having our own, will allow us to customize it in the future if needed. + */ +class GitbookIncrementalCache implements IncrementalCache { + name = 'GitbookIncrementalCache'; + + protected localCache: Cache | undefined; + + async get( + key: string, + cacheType?: CacheType + ): Promise> | 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( + key: string, + value: CacheValue, + cacheType?: CacheType + ): Promise { + const cacheKey = this.getR2Key(key, cacheType); + return trace( + { + operation: 'openNextIncrementalCacheSet', + name: cacheKey, + }, + async (span) => { + span.setAttribute('cacheType', cacheType ?? 'cache'); + const r2 = getCloudflareContext().env[BINDING_NAME]; + const localCache = await this.getCacheInstance(); + if (!r2) throw new Error('No R2 bucket'); + + try { + await r2.put(cacheKey, JSON.stringify(value)); + + //TODO: Check if there is any places where we don't have tags + // Ideally we should always have tags, but in case we don't, we need to decide how to handle it + // For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment + const tags = this.getTagsFromCacheEntry(value) ?? [ + `build_id/${process.env.NEXT_BUILD_ID}`, + ]; + + // We consider R2 as the source of truth, so we update the local cache + // only after a successful R2 write + await localCache.put( + this.getCacheUrlKey(cacheKey), + new Response( + JSON.stringify({ + value, + // Note: `Date.now()` returns the time of the last IO rather than the actual time. + // See https://developers.cloudflare.com/workers/reference/security-model/ + lastModified: Date.now(), + }), + { + headers: { + // Cache-Control default to 30 minutes, will be overridden by `revalidate` + // In theory we should always get the `revalidate` value + 'cache-control': `max-age=${value.revalidate ?? 60 * 30}`, + 'cache-tag': tags.join(','), + }, + } + ) + ); + } catch (e) { + console.error('Failed to set to cache', e); + } + } + ); + } + + async delete(key: string): Promise { + const cacheKey = this.getR2Key(key); + return trace( + { + operation: 'openNextIncrementalCacheDelete', + name: cacheKey, + }, + async () => { + const r2 = getCloudflareContext().env[BINDING_NAME]; + const localCache = await this.getCacheInstance(); + if (!r2) throw new Error('No R2 bucket'); + + try { + await r2.delete(cacheKey); + + // Here again R2 is the source of truth, so we delete from local cache first + await localCache.delete(this.getCacheUrlKey(cacheKey)); + } catch (e) { + console.error('Failed to delete from cache', e); + } + } + ); + } + + async getCacheInstance(): Promise { + 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( + entry: CacheValue + ): string[] | undefined { + if ('tags' in entry && entry.tags) { + return entry.tags; + } + + if ('meta' in entry && entry.meta && 'headers' in entry.meta && entry.meta.headers) { + const rawTags = entry.meta.headers['x-next-cache-tags']; + if (typeof rawTags === 'string') { + return rawTags.split(','); + } + } + if ('value' in entry) { + return entry.tags; + } + } +} + +export default new GitbookIncrementalCache(); diff --git a/packages/gitbook-v2/openNext/queue.ts b/packages/gitbook-v2/openNext/queue.ts new file mode 100644 index 000000000..ab33c479d --- /dev/null +++ b/packages/gitbook-v2/openNext/queue.ts @@ -0,0 +1,17 @@ +import type { Queue } from '@opennextjs/aws/types/overrides.js'; +import { getCloudflareContext } from '@opennextjs/cloudflare'; +import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue'; +import memoryQueue from '@opennextjs/cloudflare/overrides/queue/memory-queue'; + +interface Env { + IS_PREVIEW?: string; +} + +export default { + name: 'GitbookISRQueue', + send: async (msg) => { + const { ctx, env } = getCloudflareContext(); + const isPreview = (env as Env).IS_PREVIEW === 'true'; + ctx.waitUntil(isPreview ? memoryQueue.send(msg) : doQueue.send(msg)); + }, +} satisfies Queue; diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 1b97ed791..5561b7f8d 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -1,23 +1,23 @@ { "name": "gitbook-v2", - "version": "0.2.3", + "version": "0.3.0", "private": true, "dependencies": { - "next": "^15.2.3", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-tags": "workspace:*", + "@opennextjs/cloudflare": "1.0.4", "@sindresorhus/fnv1a": "^3.1.0", - "server-only": "^0.0.1", - "warn-once": "^0.1.1", - "rison": "^0.1.1", + "assert-never": "^1.2.1", "jwt-decode": "^4.0.0", - "p-memoize": "^7.1.1" + "next": "^15.3.2", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rison": "^0.1.1", + "server-only": "^0.0.1", + "warn-once": "^0.1.1" }, "devDependencies": { "gitbook": "*", - "@opennextjs/cloudflare": "^0.5.10", "@types/rison": "^0.0.9", "tailwindcss": "^3.4.0", "postcss": "^8" @@ -28,8 +28,8 @@ "build": "next build", "build:v2": "next build", "start": "next start", - "build:v2:cloudflare": "opennextjs-cloudflare", - "dev:v2:cloudflare": "wrangler dev --port 8771", + "build:v2:cloudflare": "opennextjs-cloudflare build", + "dev:v2:cloudflare": "wrangler dev --port 8771 --env preview", "unit": "bun test", "typecheck": "tsc --noEmit" } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx index e5d4ebc21..a35ecb69c 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx @@ -3,10 +3,9 @@ import { generateSitePageMetadata, generateSitePageViewport, } from '@/components/SitePage'; -import { getCacheTag } from '@gitbook/cache-tags'; import { type RouteParams, getPagePathFromParams, getStaticSiteContext } from '@v2/app/utils'; + import type { Metadata, Viewport } from 'next'; -import { unstable_cacheTag as cacheTag } from 'next/cache'; export const dynamic = 'force-static'; @@ -15,19 +14,10 @@ type PageProps = { }; export default async function Page(props: PageProps) { - 'use cache'; - const params = await props.params; const { context } = await getStaticSiteContext(params); const pathname = getPagePathFromParams(params); - cacheTag( - getCacheTag({ - tag: 'site', - site: context.site.id, - }) - ); - return ; } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx index 86a6b867f..c8f1aefe4 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx @@ -4,10 +4,8 @@ import { generateSiteLayoutMetadata, generateSiteLayoutViewport, } from '@/components/SiteLayout'; -import { getCacheTag } from '@gitbook/cache-tags'; import { type RouteLayoutParams, getStaticSiteContext } from '@v2/app/utils'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; -import { unstable_cacheTag as cacheTag } from 'next/cache'; interface SiteStaticLayoutProps { params: Promise; @@ -17,17 +15,8 @@ export default async function SiteStaticLayout({ params, children, }: React.PropsWithChildren) { - 'use cache'; - const { context, visitorAuthClaims } = await getStaticSiteContext(await params); - cacheTag( - getCacheTag({ - tag: 'site', - site: context.site.id, - }) - ); - return ( { const urlObject = new URL(rawURL); - return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}`; + return `/url/${urlObject.host}${urlObject.pathname}${urlObject.search}${urlObject.hash}`; }; } @@ -242,19 +243,45 @@ export async function fetchSiteContextByIds( ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection) : null; - const siteSpace = ( - siteStructure.type === 'siteSpaces' && siteStructure.structure - ? siteStructure.structure - : sections?.current.siteSpaces - )?.find((siteSpace) => siteSpace.id === ids.siteSpace); - if (!siteSpace) { - throw new Error('Site space not found'); - } + // Parse the current siteSpace and siteSpaces based on the site structure type. + const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => { + if (siteStructure.type === 'siteSpaces') { + const siteSpaces = siteStructure.structure; + const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace); + + if (!siteSpace) { + throw new Error( + `Site space "${ids.siteSpace}" not found in structure type="siteSpaces"` + ); + } + + return { siteSpaces, siteSpace }; + } + + if (siteStructure.type === 'sections') { + assert( + sections, + `cannot find site space "${ids.siteSpace}" because parsed sections are missing siteStructure.type="sections" siteSection="${ids.siteSection}"` + ); - const siteSpaces = - siteStructure.type === 'siteSpaces' - ? siteStructure.structure - : (sections?.current.siteSpaces ?? []); + const currentSection = sections.current; + const siteSpaces = currentSection.siteSpaces; + const siteSpace = currentSection.siteSpaces.find( + (siteSpace) => siteSpace.id === ids.siteSpace + ); + + if (!siteSpace) { + throw new Error( + `Site space "${ids.siteSpace}" not found in structure type="sections" currentSection="${currentSection.id}"` + ); + } + + return { siteSpaces, siteSpace }; + } + + // @ts-expect-error + assertNever(siteStructure, `cannot handle site structure of type ${siteStructure.type}`); + })(); const customization = (() => { if (ids.siteSpace) { @@ -380,7 +407,7 @@ export function checkIsRootSiteContext(context: GitBookSiteContext): boolean { function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false }); const section = parseCurrentSection(structure, siteSectionId); - assert(section, 'A section must be defined when there are multiple sections'); + assert(section, `couldn't find section "${siteSectionId}" in site structure`); return { list: sectionsAndGroups, current: section } satisfies SiteSections; } diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 914f9be5b..7a2d03d16 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -3,13 +3,15 @@ import { type ComputedContentSource, GitBookAPI, type GitBookAPIServiceBinding, + type HttpResponse, type RenderIntegrationUI, } from '@gitbook/api'; import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; +import { getCloudflareContext, getCloudflareRequestGlobal } from './cloudflare'; import { DataFetcherError, wrapDataFetcherError } from './errors'; -import { memoize } from './memoize'; +import { withCacheKey, withoutConcurrentExecution } from './memoize'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -19,6 +21,15 @@ interface DataFetcherInput { apiToken: string | null; } +/** + * Options to pass to the `fetch` call to disable the Next data-cache when wrapped in `use cache`. + */ +export const noCacheFetchOptions: Partial = { + next: { + revalidate: 0, + }, +}; + /** * Create a data fetcher using an API token. * The data are being cached by Next.js built-in cache. @@ -179,312 +190,391 @@ export function createDataFetcher( getUserById(userId) { return trace('getUserById', () => getUserById(input, { userId })); }, + + streamAIResponse(params) { + return streamAIResponse(input, params); + }, }; } -const getUserById = memoize(async function getUserById( - input: DataFetcherInput, - params: { userId: string } -) { - 'use cache'; - - return trace('getUserById.uncached', () => { - cacheLife('days'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.users.getUserById(params.userId); - return res.data; - }); - }); -}); - -const getSpace = memoize(async function getSpace( - input: DataFetcherInput, - params: { - spaceId: string; - shareKey: string | undefined; - } -) { - 'use cache'; - - return trace('getSpace.uncached', () => { - cacheLife('days'); - cacheTag( - getCacheTag({ - tag: 'space', - space: params.spaceId, - }) - ); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getSpaceById(params.spaceId, { - shareKey: params.shareKey, +const getUserById = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async (_, input: DataFetcherInput, params: { userId: string }) => { + 'use cache'; + return trace(`getUserById(${params.userId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.users.getUserById(params.userId, { + ...noCacheFetchOptions, + }); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); }); - return res.data; - }); - }); -}); - -const getChangeRequest = memoize(async function getChangeRequest( - input: DataFetcherInput, - params: { - spaceId: string; - changeRequestId: string; - } -) { - 'use cache'; - - return trace('getChangeRequest.uncached', () => { - cacheLife('minutes'); + } + ) +); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId - ); +const getSpace = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; shareKey: string | undefined } + ) => { + 'use cache'; cacheTag( getCacheTag({ - tag: 'change-request', + tag: 'space', space: params.spaceId, - changeRequest: res.data.id, }) ); - return res.data; - }); - }); -}); - -const getRevision = memoize(async function getRevision( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - metadata: boolean; - } -) { - 'use cache'; - return trace('getRevision.uncached', () => { - cacheLife('max'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getRevisionById(params.spaceId, params.revisionId, { - metadata: params.metadata, + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getSpaceById( + params.spaceId, + { + shareKey: params.shareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); }); - return res.data; - }); - }); -}); - -const getRevisionPages = memoize(async function getRevisionPages( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - metadata: boolean; - } -) { - 'use cache'; + } + ) +); - return trace('getRevisionPages.uncached', () => { - cacheLife('max'); +const getChangeRequest = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; changeRequestId: string } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'change-request', + space: params.spaceId, + changeRequest: params.changeRequestId, + }) + ); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.listPagesInRevisionById( - params.spaceId, - params.revisionId, - { - metadata: params.metadata, + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + }); } ); - return res.data.pages; - }); - }); -}); + } + ) +); -const getRevisionFile = memoize(async function getRevisionFile( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - fileId: string; - } -) { - 'use cache'; +const getRevision = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + }); + } + ) +); - return trace('getRevisionFile.uncached', () => { - cacheLife('max'); +const getRevisionPages = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; metadata: boolean } + ) => { + 'use cache'; + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data.pages; + }); + }); + } + ) +); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getFileInRevisionById( - params.spaceId, - params.revisionId, - params.fileId, - {} +const getRevisionFile = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; fileId: string } + ) => { + 'use cache'; + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } ); - return res.data; - }); - }); -}); + } + ) +); -const getRevisionPageMarkdown = memoize(async function getRevisionPageMarkdown( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - pageId: string; - } -) { - 'use cache'; - - return trace('getRevisionPageMarkdown.uncached', () => { - cacheLife('max'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getPageInRevisionById( - params.spaceId, - params.revisionId, - params.pageId, - { - format: 'markdown', +const getRevisionPageMarkdown = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; pageId: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionById( + params.spaceId, + params.revisionId, + params.pageId, + { + format: 'markdown', + }, + { + ...noCacheFetchOptions, + } + ); + + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + + if (!('markdown' in res.data)) { + throw new DataFetcherError('Page is not a document', 404); + } + return res.data.markdown; + }); } ); + } + ) +); - if (!('markdown' in res.data)) { - throw new DataFetcherError('Page is not a document', 404); - } - - return res.data.markdown; - }); - }); -}); - -const getRevisionPageByPath = memoize(async function getRevisionPageByPath( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - path: string; - } -) { - 'use cache'; - - return trace('getRevisionPageByPath.uncached', () => { - cacheLife('max'); - - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getPageInRevisionByPath( - params.spaceId, - params.revisionId, - encodedPath, - {} +const getRevisionPageByPath = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; path: string } + ) => { + 'use cache'; + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } ); + } + ) +); - return res.data; - }); - }); -}); - -const getDocument = memoize(async function getDocument( - input: DataFetcherInput, - params: { - spaceId: string; - documentId: string; - } -) { - 'use cache'; - - return trace('getDocument.uncached', () => { - cacheLife('max'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getDocumentById(params.spaceId, params.documentId, {}); - return res.data; - }); - }); -}); - -const getComputedDocument = memoize(async function getComputedDocument( - input: DataFetcherInput, - params: { - spaceId: string; - organizationId: string; - source: ComputedContentSource; - seed: string; - } -) { - 'use cache'; - - return trace('getComputedDocument.uncached', () => { - cacheLife('days'); - - cacheTag( - ...getComputedContentSourceCacheTags( - { - spaceId: params.spaceId, - organizationId: params.organizationId, - }, - params.source - ) - ); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getComputedDocument(params.spaceId, { - source: params.source, - seed: params.seed, +const getDocument = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async (_, input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { + 'use cache'; + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getDocumentById( + params.spaceId, + params.documentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); }); - return res.data; - }); - }); -}); + } + ) +); -const getReusableContent = memoize(async function getReusableContent( - input: DataFetcherInput, - params: { - spaceId: string; - revisionId: string; - reusableContentId: string; - } -) { - 'use cache'; +const getComputedDocument = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { + spaceId: string; + organizationId: string; + source: ComputedContentSource; + seed: string; + } + ) => { + 'use cache'; + cacheTag( + ...getComputedContentSourceCacheTags( + { + spaceId: params.spaceId, + organizationId: params.organizationId, + }, + params.source + ) + ); - return trace('getReusableContent.uncached', () => { - cacheLife('max'); + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getComputedDocument( + params.spaceId, + { + source: params.source, + seed: params.seed, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } + ); + } + ) +); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getReusableContentInRevisionById( - params.spaceId, - params.revisionId, - params.reusableContentId +const getReusableContent = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { spaceId: string; revisionId: string; reusableContentId: string } + ) => { + 'use cache'; + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } ); - return res.data; - }); - }); -}); - -const getLatestOpenAPISpecVersionContent = memoize( - async function getLatestOpenAPISpecVersionContent( - input: DataFetcherInput, - params: { - organizationId: string; - slug: string; } - ) { - 'use cache'; + ) +); - return trace('getLatestOpenAPISpecVersionContent.uncached', () => { +const getLatestOpenAPISpecVersionContent = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async (_, input: DataFetcherInput, params: { organizationId: string; slug: string }) => { + 'use cache'; cacheTag( getCacheTag({ tag: 'openapi', @@ -492,173 +582,265 @@ const getLatestOpenAPISpecVersionContent = memoize( openAPISpec: params.slug, }) ); - cacheLife('days'); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.orgs.getLatestOpenApiSpecVersionContent( - params.organizationId, - params.slug - ); - return res.data; - }); - }); - } + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('max'); + return res.data; + }); + } + ); + } + ) ); -const getPublishedContentSite = memoize(async function getPublishedContentSite( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - } -) { - 'use cache'; - - return trace('getPublishedContentSite.uncached', () => { - cacheLife('days'); - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - - return trace('getPublishedContentSite', () => { - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.orgs.getPublishedContentSite( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - } - ); - return res.data; - }); - }); - }); -}); +const getPublishedContentSite = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { organizationId: string; siteId: string; siteShareKey: string | undefined } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); -const getSiteRedirectBySource = memoize(async function getSiteRedirectBySource( - input: DataFetcherInput, - params: { - organizationId: string; - siteId: string; - siteShareKey: string | undefined; - source: string; - } -) { - 'use cache'; - - return trace('getSiteRedirectBySource.uncached', () => { - cacheTag( - getCacheTag({ - tag: 'site', - site: params.siteId, - }) - ); - cacheLife('days'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.orgs.getSiteRedirectBySource( - params.organizationId, - params.siteId, - { - shareKey: params.siteShareKey, - source: params.source, + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); } ); + } + ) +); - return res.data; - }); - }); -}); +const getSiteRedirectBySource = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; + } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); -const getEmbedByUrl = memoize(async function getEmbedByUrl( - input: DataFetcherInput, - params: { - url: string; - spaceId: string; - } -) { - 'use cache'; + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); + } + ); + } + ) +); - return trace('getEmbedByUrl.uncached', () => { - cacheLife('weeks'); +const getEmbedByUrl = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async (_, input: DataFetcherInput, params: { spaceId: string; url: string }) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'space', + space: params.spaceId, + }) + ); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url }); - return res.data; - }); - }); -}); + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.spaces.getEmbedByUrlInSpace( + params.spaceId, + { + url: params.url, + }, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('weeks'); + return res.data; + }); + }); + } + ) +); -const searchSiteContent = memoize(async function searchSiteContent( - input: DataFetcherInput, - params: Parameters[0] -) { - 'use cache'; +const searchSiteContent = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: Parameters[0] + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'site', + site: params.siteId, + }) + ); - return trace('searchSiteContent.uncached', () => { - const { organizationId, siteId, query, scope } = params; + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { + return wrapDataFetcherError(async () => { + const { organizationId, siteId, query, scope } = params; + const api = apiClient(input); + const res = await api.orgs.searchSiteContent( + organizationId, + siteId, + { + query, + ...scope, + }, + {}, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('hours'); + return res.data.items; + }); + } + ); + } + ) +); - cacheLife('days'); +const renderIntegrationUi = withCacheKey( + withoutConcurrentExecution( + getCloudflareRequestGlobal, + async ( + _, + input: DataFetcherInput, + params: { integrationName: string; request: RenderIntegrationUI } + ) => { + 'use cache'; + cacheTag( + getCacheTag({ + tag: 'integration', + integration: params.integrationName, + }) + ); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.orgs.searchSiteContent(organizationId, siteId, { - query, - ...scope, + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { + return wrapDataFetcherError(async () => { + const api = apiClient(input); + const res = await api.integrations.renderIntegrationUiWithPost( + params.integrationName, + params.request, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('days'); + return res.data; + }); }); - return res.data.items; - }); - }); -}); + } + ) +); -const renderIntegrationUi = memoize(async function renderIntegrationUi( +async function* streamAIResponse( input: DataFetcherInput, - params: { - integrationName: string; - request: RenderIntegrationUI; - } + params: Parameters[0] ) { - 'use cache'; - - return trace('renderIntegrationUi.uncached', () => { - cacheTag(getCacheTag({ tag: 'integration', integration: params.integrationName })); - cacheLife('days'); + const api = apiClient(input); + const res = await api.orgs.streamAiResponseInSite( + params.organizationId, + params.siteId, + { + input: params.input, + output: params.output, + model: params.model, + }, + { + ...noCacheFetchOptions, + } + ); - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.integrations.renderIntegrationUiWithPost( - params.integrationName, - params.request - ); - return res.data; - }); - }); -}); + for await (const event of res) { + yield event; + } +} let loggedServiceBinding = false; /** * Create a new API client. */ -export async function apiClient(input: DataFetcherInput = { apiToken: null }) { +export function apiClient(input: DataFetcherInput = { apiToken: null }) { const { apiToken } = input; let serviceBinding: GitBookAPIServiceBinding | undefined; - try { - // HACK: This is a workaround to avoid webpack trying to bundle this cloudflare only module - // @ts-ignore - const { env } = await import( - /* webpackIgnore: true */ `${'__cloudflare:workers'.replaceAll('_', '')}` - ); - serviceBinding = env.GITBOOK_API; + const cloudflareContext = getCloudflareContext(); + if (cloudflareContext) { + // @ts-expect-error + serviceBinding = cloudflareContext.env.GITBOOK_API as GitBookAPIServiceBinding | undefined; if (!loggedServiceBinding) { loggedServiceBinding = true; if (serviceBinding) { @@ -669,10 +851,6 @@ export async function apiClient(input: DataFetcherInput = { apiToken: null }) { console.warn(`no service binding for the API (${GITBOOK_API_URL})`); } } - } catch (error) { - if (process.env.NODE_ENV === 'production' && !process.env.VERCEL) { - throw error; - } } const api = new GitBookAPI({ @@ -684,3 +862,12 @@ export async function apiClient(input: DataFetcherInput = { apiToken: null }) { return api; } + +/** + * Get the tags from the API responses. + */ +function getCacheTagsFromResponse(response: HttpResponse) { + const cacheTagHeader = response.headers.get('x-gitbook-cache-tag'); + const tags = !cacheTagHeader ? [] : cacheTagHeader.split(','); + return tags; +} diff --git a/packages/gitbook-v2/src/lib/data/cloudflare.ts b/packages/gitbook-v2/src/lib/data/cloudflare.ts new file mode 100644 index 000000000..ac3125603 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/cloudflare.ts @@ -0,0 +1,25 @@ +import { getCloudflareContext as getCloudflareContextOpenNext } from '@opennextjs/cloudflare'; +import { GITBOOK_RUNTIME } from '../env'; + +/** + * Return the Cloudflare context or null when not running in Cloudflare. + */ +export function getCloudflareContext() { + if (GITBOOK_RUNTIME !== 'cloudflare') { + return null; + } + + return getCloudflareContextOpenNext(); +} + +/** + * Return an object representing the current request. + */ +export function getCloudflareRequestGlobal() { + const context = getCloudflareContext(); + if (!context) { + return null; + } + + return context.cf; +} diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts index 0c083fa3e..784eebdbb 100644 --- a/packages/gitbook-v2/src/lib/data/errors.ts +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -98,6 +98,10 @@ export async function wrapDataFetcherError( */ export function getExposableError(error: Error): DataFetcherErrorData { if (error instanceof GitBookAPIError) { + if (error.code >= 500) { + throw error; + } + return { code: error.code, message: error.errorMessage, @@ -105,6 +109,10 @@ export function getExposableError(error: Error): DataFetcherErrorData { } if (error instanceof DataFetcherError) { + if (error.code >= 500) { + throw error; + } + return { code: error.code, message: error.message, diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts index 12e06f8cf..d79049684 100644 --- a/packages/gitbook-v2/src/lib/data/index.ts +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -4,3 +4,5 @@ export * from './pages'; export * from './urls'; export * from './errors'; export * from './lookup'; +export * from './proxy'; +export * from './visitor'; diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts index 498f6e885..4c999bd7a 100644 --- a/packages/gitbook-v2/src/lib/data/lookup.ts +++ b/packages/gitbook-v2/src/lib/data/lookup.ts @@ -1,55 +1,102 @@ import { race, tryCatch } from '@/lib/async'; import { joinPath, joinPathWithBaseURL } from '@/lib/paths'; import { trace } from '@/lib/tracing'; -import type { PublishedSiteContentLookup } from '@gitbook/api'; +import type { GitBookAPI, PublishedSiteContentLookup, SiteVisitorPayload } from '@gitbook/api'; import { apiClient } from './api'; import { getExposableError } from './errors'; import type { DataFetcherResponse } from './types'; import { getURLLookupAlternatives, stripURLSearch } from './urls'; +interface LookupPublishedContentByUrlInput { + url: string; + redirectOnError: boolean; + apiToken: string | null; + visitorPayload: SiteVisitorPayload; +} + +/** + * Lookup a content by its URL using the GitBook resolvePublishedContentByUrl API endpoint. + * To optimize caching, we try multiple lookup alternatives and return the first one that matches. + */ +export async function resolvePublishedContentByUrl(input: LookupPublishedContentByUrlInput) { + return lookupPublishedContentByUrl({ + url: input.url, + fetchLookupAPIResult: ({ url, signal }) => { + const api = apiClient({ apiToken: input.apiToken }); + return trace( + { + operation: 'resolvePublishedContentByUrl', + name: url, + }, + () => + tryCatch( + api.urls.resolvePublishedContentByUrl( + { + url, + ...(input.visitorPayload ? { visitor: input.visitorPayload } : {}), + redirectOnError: input.redirectOnError, + }, + { signal } + ) + ) + ); + }, + }); +} + /** - * Lookup a content by its URL using the GitBook API. + * Lookup a content by its URL using the GitBook getPublishedContentByUrl API endpoint. * To optimize caching, we try multiple lookup alternatives and return the first one that matches. + * + * @deprecated use resolvePublishedContentByUrl. + * */ -export async function getPublishedContentByURL(input: { +export async function getPublishedContentByURL(input: LookupPublishedContentByUrlInput) { + return lookupPublishedContentByUrl({ + url: input.url, + fetchLookupAPIResult: ({ url, signal }) => { + const api = apiClient({ apiToken: input.apiToken }); + return trace( + { + operation: 'getPublishedContentByURL', + name: url, + }, + () => + tryCatch( + api.urls.getPublishedContentByUrl( + { + url, + visitorAuthToken: input.visitorPayload.jwtToken ?? undefined, + redirectOnError: input.redirectOnError, + // @ts-expect-error - cacheVersion is not a real query param + cacheVersion: 'v2', + }, + { signal } + ) + ) + ); + }, + }); +} + +type TryCatch = ReturnType>; + +async function lookupPublishedContentByUrl(input: { url: string; - visitorAuthToken: string | null; - redirectOnError: boolean; - apiToken: string | null; + fetchLookupAPIResult: (args: { + url: string; + signal: AbortSignal; + }) => TryCatch>>; }): Promise> { const lookupURL = new URL(input.url); const url = stripURLSearch(lookupURL); const lookup = getURLLookupAlternatives(url); const result = await race(lookup.urls, async (alternative, { signal }) => { - const api = await apiClient({ apiToken: input.apiToken }); - - const callResult = await trace( - { - operation: 'getPublishedContentByURL', - name: alternative.url, - }, - () => - tryCatch( - api.urls.getPublishedContentByUrl( - { - url: alternative.url, - visitorAuthToken: input.visitorAuthToken ?? undefined, - redirectOnError: input.redirectOnError, - - // As this endpoint is cached by our API, we version the request - // to void getting stale data with missing properties. - // this could be improved by ensuring our API cache layer is versioned - // or invalidated when needed - // @ts-expect-error - cacheVersion is not a real query param - cacheVersion: 'v2', - }, - { - signal, - } - ) - ) - ); + const callResult = await input.fetchLookupAPIResult({ + url: alternative.url, + signal, + }); if (callResult.error) { if (alternative.primary) { diff --git a/packages/gitbook-v2/src/lib/data/memoize.test.ts b/packages/gitbook-v2/src/lib/data/memoize.test.ts index 4764d52a7..24c101408 100644 --- a/packages/gitbook-v2/src/lib/data/memoize.test.ts +++ b/packages/gitbook-v2/src/lib/data/memoize.test.ts @@ -1,39 +1,52 @@ import { describe, expect, it, mock } from 'bun:test'; -import { memoize } from './memoize'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { withCacheKey, withoutConcurrentExecution } from './memoize'; -describe('memoize', () => { - it('should memoize the function', async () => { - const fn = mock(async () => Math.random()); - const memoized = memoize(fn); - expect(await memoized()).toBe(await memoized()); - }); +describe('withoutConcurrentExecution', () => { + it('should memoize the function based on the cache key', async () => { + const fn = mock(async (_cacheKey: string, a: number, b: number) => a + b); + const memoized = withoutConcurrentExecution(() => null, fn); - it('should memoize the function with different arguments', async () => { - const fn = mock(async (a: number, b: number) => a + b); - const memoized = memoize(fn); - expect(await memoized(1, 2)).toBe(await memoized(1, 2)); - expect(fn.mock.calls.length).toBe(1); - expect(await memoized(1, 2)).not.toBe(await memoized(2, 3)); - expect(fn.mock.calls.length).toBe(2); - }); + const p1 = memoized('c1', 1, 2); + const p2 = memoized('c1', 1, 2); + const p3 = memoized('c3', 2, 3); - it('should memoize a function complex object', async () => { - const fn = mock(async (a: { foo: string; bar: number }) => a.foo + a.bar); - const memoized = memoize(fn); - expect(await memoized({ foo: 'foo', bar: 1 })).toBe(await memoized({ foo: 'foo', bar: 1 })); - expect(fn.mock.calls.length).toBe(1); - expect(await memoized({ foo: 'foo', bar: 1 })).not.toBe( - await memoized({ foo: 'foo', bar: 2 }) - ); + expect(await p1).toBe(await p2); + expect(await p1).not.toBe(await p3); expect(fn.mock.calls.length).toBe(2); }); - it('should wrap concurrent async calls', async () => { + it('should support caching per request', async () => { const fn = mock(async () => Math.random()); - const memoized = memoize(fn); - const promise1 = memoized(); - const promise2 = memoized(); + + const request1 = { id: 'request1' }; + const request2 = { id: 'request2' }; + + const requestContext = new AsyncLocalStorage<{ id: string }>(); + + const memoized = withoutConcurrentExecution(() => requestContext.getStore(), fn); + + // Both in the same request + const promise1 = requestContext.run(request1, () => memoized('c1')); + const promise2 = requestContext.run(request1, () => memoized('c1')); + + // In a different request + const promise3 = requestContext.run(request2, () => memoized('c1')); + expect(await promise1).toBe(await promise2); + expect(await promise1).not.toBe(await promise3); + expect(fn.mock.calls.length).toBe(2); + }); +}); + +describe('withCacheKey', () => { + it('should wrap the function by passing the cache key', async () => { + const fn = mock( + async (cacheKey: string, arg: { a: number; b: number }, c: number) => + `${cacheKey}, result=${arg.a + arg.b + c}` + ); + const memoized = withCacheKey(fn); + expect(await memoized({ a: 1, b: 2 }, 4)).toBe('[[["a",1],["b",2]],4], result=7'); expect(fn.mock.calls.length).toBe(1); }); }); diff --git a/packages/gitbook-v2/src/lib/data/memoize.ts b/packages/gitbook-v2/src/lib/data/memoize.ts index 9b5ca74d7..ff1ff5e84 100644 --- a/packages/gitbook-v2/src/lib/data/memoize.ts +++ b/packages/gitbook-v2/src/lib/data/memoize.ts @@ -1,17 +1,61 @@ -import pMemoize from 'p-memoize'; +/** + * Wrap a function by preventing concurrent executions of the same function. + * With a logic to work per-request in Cloudflare Workers. + */ +export function withoutConcurrentExecution( + getGlobalContext: () => object | null | undefined, + wrapped: (key: string, ...args: ArgsType) => Promise +): (cacheKey: string, ...args: ArgsType) => Promise { + const globalPromiseCache = new WeakMap>>(); + + return (key: string, ...args: ArgsType) => { + const globalContext = getGlobalContext() ?? globalThis; + + /** + * Cache storage that is scoped to the current request when executed in Cloudflare Workers, + * to avoid "Cannot perform I/O on behalf of a different request" errors. + */ + const promiseCache = + globalPromiseCache.get(globalContext) ?? new Map>(); + globalPromiseCache.set(globalContext, promiseCache); + + const concurrent = promiseCache.get(key); + if (concurrent) { + return concurrent; + } + + const promise = (async () => { + try { + const result = await wrapped(key, ...args); + return result; + } finally { + promiseCache.delete(key); + } + })(); + + promiseCache.set(key, promise); + + return promise; + }; +} + +/** + * Wrap a function by passing it a cache key that is computed from the function arguments. + */ +export function withCacheKey( + wrapped: (cacheKey: string, ...args: ArgsType) => Promise +): (...args: ArgsType) => Promise { + return (...args: ArgsType) => { + const cacheKey = getCacheKey(args); + return wrapped(cacheKey, ...args); + }; +} /** - * We wrap 'use cache' calls in a p-memoize function to avoid - * executing the function multiple times when doing concurrent calls. - * - * Hopefully one day this can be done directly by 'use cache'. + * Compute a cache key from the function arguments. */ -export function memoize any>(f: F): F { - return pMemoize(f, { - cacheKey: (args) => { - return JSON.stringify(deepSortValue(args)); - }, - }); +function getCacheKey(args: any[]) { + return JSON.stringify(deepSortValue(args)); } function deepSortValue(value: unknown): unknown { diff --git a/packages/gitbook-v2/src/lib/data/proxy.test.ts b/packages/gitbook-v2/src/lib/data/proxy.test.ts new file mode 100644 index 000000000..caf750555 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/proxy.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'bun:test'; +import { getProxyRequestIdentifier, isProxyRequest } from './proxy'; + +describe('isProxyRequest', () => { + it('should return true for proxy requests', () => { + const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'); + expect(isProxyRequest(proxyRequestURL)).toBe(true); + }); + + it('should return false for non-proxy requests', () => { + const nonProxyRequestURL = new URL('https://example.com/docs/foo/hello/world'); + expect(isProxyRequest(nonProxyRequestURL)).toBe(false); + }); +}); + +describe('getProxyRequestIdentifier', () => { + it('should return the correct identifier for proxy requests', () => { + const proxyRequestURL = new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'); + expect(getProxyRequestIdentifier(proxyRequestURL)).toBe('sites/site_foo'); + }); +}); diff --git a/packages/gitbook-v2/src/lib/data/proxy.ts b/packages/gitbook-v2/src/lib/data/proxy.ts new file mode 100644 index 000000000..665466503 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/proxy.ts @@ -0,0 +1,15 @@ +/** + * Check if the request to the site was through a proxy. + */ +export function isProxyRequest(requestURL: URL): boolean { + return ( + requestURL.host === 'proxy.gitbook.site' || requestURL.host === 'proxy.gitbook-staging.site' + ); +} + +export function getProxyRequestIdentifier(requestURL: URL): string { + // For proxy requests, we extract the site ID from the pathname + // e.g. https://proxy.gitbook.site/site/siteId/... + const pathname = requestURL.pathname.slice(1).split('/'); + return pathname.slice(0, 2).join('/'); +} diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 2573dbcb0..178a0ba77 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -179,4 +179,15 @@ export interface GitBookDataFetcher { integrationName: string; request: api.RenderIntegrationUI; }): Promise>; + + /** + * Stream an AI response. + */ + streamAIResponse(params: { + organizationId: string; + siteId: string; + input: api.AIMessageInput[]; + output: api.AIOutputFormat; + model: api.AIModel; + }): AsyncGenerator; } diff --git a/packages/gitbook-v2/src/lib/data/visitor.test.ts b/packages/gitbook-v2/src/lib/data/visitor.test.ts new file mode 100644 index 000000000..373dcbc47 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/visitor.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'bun:test'; +import { getVisitorAuthBasePath } from './visitor'; + +describe('getVisitorAuthBasePath', () => { + it('should return the correct base path for proxy requests', () => { + expect( + getVisitorAuthBasePath( + new URL('https://proxy.gitbook.site/sites/site_foo/hello/world'), + { + site: 'site_foo', + siteSpace: 'sitesp_foo', + basePath: '/foo', + siteBasePath: '/foo', + organization: 'org_foo', + space: 'space_foo', + pathname: '/hello/world', + complete: false, + apiToken: 'api_token_foo', + canonicalUrl: 'https://example.com/docs/foo/hello/world', + } + ) + ).toBe('/sites/site_foo/'); + }); + + it('should return the correct base path for non-proxy requests', () => { + expect( + getVisitorAuthBasePath(new URL('https://example.com/docs/foo/hello/world'), { + site: 'site_foo', + siteSpace: 'sitesp_foo', + basePath: '/foo/', + siteBasePath: '/foo/', + organization: 'org_foo', + space: 'space_foo', + pathname: '/hello/world', + complete: false, + apiToken: 'api_token_foo', + canonicalUrl: 'https://example.com/docs/foo/hello/world', + }) + ).toBe('/foo/'); + }); +}); diff --git a/packages/gitbook-v2/src/lib/data/visitor.ts b/packages/gitbook-v2/src/lib/data/visitor.ts new file mode 100644 index 000000000..f93f0afe8 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/visitor.ts @@ -0,0 +1,19 @@ +import { withLeadingSlash, withTrailingSlash } from '@/lib/paths'; +import type { PublishedSiteContent } from '@gitbook/api'; +import { getProxyRequestIdentifier, isProxyRequest } from './proxy'; + +/** + * Get the appropriate base path for the visitor authentication cookie. + */ +export function getVisitorAuthBasePath( + siteRequestURL: URL, + siteURLData: PublishedSiteContent +): string { + // The siteRequestURL for proxy requests is of the form `https://proxy.gitbook.com/site/siteId/...` + // In such cases, we should not use the resolved siteBasePath for the cookie because for subsequent requests + // we will not have the siteBasePath in the request URL in order to retrieve the cookie. So we use the + // proxy identifier instead. + return isProxyRequest(siteRequestURL) + ? withLeadingSlash(withTrailingSlash(getProxyRequestIdentifier(siteRequestURL))) + : siteURLData.siteBasePath; +} diff --git a/packages/gitbook-v2/src/lib/env/globals.ts b/packages/gitbook-v2/src/lib/env/globals.ts index 26fd29163..20cb84cb3 100644 --- a/packages/gitbook-v2/src/lib/env/globals.ts +++ b/packages/gitbook-v2/src/lib/env/globals.ts @@ -6,6 +6,14 @@ import 'server-only'; * and not from the `process.env` object. */ +/** + * Runtime environment. + */ +export const GITBOOK_RUNTIME = (process.env.GITBOOK_RUNTIME ?? 'unknown') as + | 'vercel' + | 'cloudflare' + | 'unknown'; + /** * Main host on which GitBook is running. */ @@ -77,6 +85,15 @@ export const GITBOOK_IMAGE_RESIZE_URL = process.env.GITBOOK_IMAGE_RESIZE_URL ?? export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY = process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null; +/** + * Mode used for resizing images. + */ +export const GITBOOK_IMAGE_RESIZE_MODE = enforceEnum( + 'GITBOOK_IMAGE_RESIZE_MODE', + process.env.GITBOOK_IMAGE_RESIZE_MODE || 'cdn-cgi', + ['cdn-cgi', 'cf-fetch'] +); + /** * Endpoint where icons are served. */ @@ -92,3 +109,12 @@ export const GITBOOK_ICONS_TOKEN = process.env.GITBOOK_ICONS_TOKEN; * Secret used to validate requests from the GitBook app. */ export const GITBOOK_SECRET = process.env.GITBOOK_SECRET ?? null; + +function enforceEnum(key: string, value: string, enumValues: T[]): T { + if (!enumValues.includes(value as T)) { + throw new Error( + `Invalid value for ${key}: "${value}", expected one of: ${enumValues.join(', ')}` + ); + } + return value as T; +} diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts new file mode 100644 index 000000000..679f2c5ce --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'bun:test'; +import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL'; + +describe('checkIsSizableImageURL', () => { + it('should return Skip for non-parsable URLs', () => { + expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for non-http(s) URLs', () => { + expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip); + expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for localhost URLs', () => { + expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe( + SizableImageAction.Skip + ); + expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip); + }); + + it('should return Skip for GitBook image URLs', () => { + expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe( + SizableImageAction.Skip + ); + }); + + it('should return Resize for supported image extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.png')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe( + SizableImageAction.Resize + ); + }); + + it('should return Resize for URLs without extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize); + }); + + it('should return Passthrough for unsupported image extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe( + SizableImageAction.Passthrough + ); + expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe( + SizableImageAction.Passthrough + ); + }); + + it('should handle URLs with query parameters correctly', () => { + expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe( + SizableImageAction.Passthrough + ); + }); + + it('should be case-insensitive for extensions', () => { + expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe( + SizableImageAction.Resize + ); + expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe( + SizableImageAction.Resize + ); + }); +}); diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts new file mode 100644 index 000000000..486ea7a69 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts @@ -0,0 +1,43 @@ +import { getExtension } from '@/lib/paths'; + +export enum SizableImageAction { + Resize = 'resize', + Skip = 'skip', + Passthrough = 'passthrough', +} + +/** + * https://developers.cloudflare.com/images/transform-images/#supported-input-formats + */ +const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + +/** + * Check if an image URL is resizable. + * Skip it for non-http(s) URLs (data, etc). + * Skip it for SVGs. + * Skip it for GitBook images (to avoid recursion). + */ +export function checkIsSizableImageURL(input: string): SizableImageAction { + if (!URL.canParse(input)) { + return SizableImageAction.Skip; + } + + const parsed = new URL(input); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return SizableImageAction.Skip; + } + if (parsed.hostname === 'localhost') { + return SizableImageAction.Skip; + } + if (parsed.pathname.includes('/~gitbook/image')) { + return SizableImageAction.Skip; + } + + 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; +} diff --git a/packages/gitbook-v2/src/lib/images/createImageResizer.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts index b8173b9ed..8507a7aef 100644 --- a/packages/gitbook-v2/src/lib/images/createImageResizer.ts +++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts @@ -1,34 +1,11 @@ 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 { getImageSize } from './resizer'; import { type SignatureVersion, generateImageSignature } from './signatures'; import type { ImageResizer } from './types'; -interface CloudflareImageJsonFormat { - width: number; - height: number; - original: { - file_size: number; - width: number; - height: number; - format: string; - }; -} - -/** - * https://developers.cloudflare.com/images/image-resizing/resize-with-workers/ - */ -export interface CloudflareImageOptions { - format?: 'webp' | 'avif' | 'json' | 'jpeg'; - fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; - width?: number; - height?: number; - dpr?: number; - anim?: boolean; - quality?: number; -} - /** * Create an image resizer for a rendering context. */ @@ -47,7 +24,7 @@ export function createImageResizer({ return { getResizedImageURL: (urlInput) => { - if (!checkIsSizableImageURL(urlInput)) { + if (checkIsSizableImageURL(urlInput) === SizableImageAction.Skip) { return null; } @@ -87,7 +64,7 @@ export function createImageResizer({ }, getImageSize: async (input, options) => { - if (!checkIsSizableImageURL(input)) { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { return null; } @@ -106,124 +83,6 @@ export function createNoopImageResizer(): ImageResizer { }; } -/** - * Check if a URL is an HTTP URL. - */ -export function checkIsHttpURL(input: string | URL): boolean { - if (!URL.canParse(input)) { - return false; - } - const parsed = new URL(input); - return parsed.protocol === 'http:' || parsed.protocol === 'https:'; -} - -/** - * Check if an image URL is resizable. - * Skip it for non-http(s) URLs (data, etc). - * Skip it for SVGs. - * Skip it for GitBook images (to avoid recursion). - */ -export function checkIsSizableImageURL(input: string): boolean { - if (!URL.canParse(input)) { - return false; - } - - if (input.includes('/~gitbook/image')) { - return false; - } - - const parsed = new URL(input); - if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) { - return false; - } - if (!checkIsHttpURL(parsed)) { - return false; - } - - return true; -} - -/** - * Get the size of an image. - */ -export async function getImageSize( - input: string, - defaultSize: Partial = {} -): Promise<{ width: number; height: number } | null> { - if (!checkIsSizableImageURL(input)) { - return null; - } - - try { - const response = await resizeImage(input, { - // Abort the request after 2 seconds to avoid blocking rendering for too long - signal: AbortSignal.timeout(2000), - // Measure size and resize it to the most common size - // to optimize caching - ...defaultSize, - format: 'json', - anim: false, - }); - - const json = (await response.json()) as CloudflareImageJsonFormat; - return { - width: json.original.width, - height: json.original.height, - }; - } catch (_error) { - return null; - } -} - -/** - * Execute a Cloudflare Image Resize operation on an image. - */ -export async function resizeImage( - input: string, - options: CloudflareImageOptions & { - signal?: AbortSignal; - } -): Promise { - const { signal, ...resizeOptions } = options; - - const parsed = new URL(input); - if (parsed.protocol === 'data:') { - throw new Error('Cannot resize data: URLs'); - } - - if (parsed.hostname === 'localhost') { - throw new Error('Cannot resize localhost URLs'); - } - - // Since Cloudflare Images options on fetch are not supported on Cloudflare Pages, - // we need to use the Cloudflare Image Resize API directly. - if (!GITBOOK_IMAGE_RESIZE_URL) { - throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set'); - } - - return await fetch( - `${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions( - resizeOptions - )}/${encodeURIComponent(input)}`, - { - headers: { - // Pass the `Accept` header, as Cloudflare uses this to validate the format. - Accept: - resizeOptions.format === 'json' - ? 'application/json' - : `image/${resizeOptions.format || 'jpeg'}`, - }, - signal, - } - ); -} - -function stringifyOptions(options: CloudflareImageOptions): string { - return Object.entries({ ...options }).reduce((rest, [key, value]) => { - return `${rest}${rest ? ',' : ''}${key}=${value}`; - }, ''); -} - /** * Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost. * We protect against it by converting to a special token, and then parsing diff --git a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts index 58b5608bb..82f922526 100644 --- a/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts +++ b/packages/gitbook-v2/src/lib/images/getImageResizingContextId.ts @@ -1,13 +1,12 @@ +import { getProxyRequestIdentifier, isProxyRequest } from '../data'; + /** * Get the site identifier to use for image resizing for an incoming request. * This identifier can be obtained before resolving the request URL. */ export function getImageResizingContextId(url: URL): string { - if (url.host === 'proxy.gitbook.site' || url.host === 'proxy.gitbook-staging.site') { - // For proxy requests, we extract the site ID from the pathname - // e.g. https://proxy.gitbook.site/site/siteId/... - const pathname = url.pathname.slice(1).split('/'); - return pathname.slice(0, 2).join('/'); + if (isProxyRequest(url)) { + return getProxyRequestIdentifier(url); } return url.host; diff --git a/packages/gitbook-v2/src/lib/images/index.ts b/packages/gitbook-v2/src/lib/images/index.ts index 9ae0a4c00..fa6511530 100644 --- a/packages/gitbook-v2/src/lib/images/index.ts +++ b/packages/gitbook-v2/src/lib/images/index.ts @@ -3,3 +3,5 @@ export * from './createImageResizer'; export * from './signatures'; export * from './utils'; export * from './getImageResizingContextId'; +export * from './resizer'; +export * from './checkIsSizableImageURL'; diff --git a/packages/gitbook-v2/src/lib/images/resizer/cdn-cgi.ts b/packages/gitbook-v2/src/lib/images/resizer/cdn-cgi.ts new file mode 100644 index 000000000..93c3aeeba --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/cdn-cgi.ts @@ -0,0 +1,48 @@ +import { GITBOOK_IMAGE_RESIZE_URL } from '@v2/lib/env'; +import type { CloudflareImageOptions } from './types'; +import { copyImageResponse } from './utils'; + +/** + * Resize an image by doing a request to a /cdn/cgi/ endpoint. + * https://developers.cloudflare.com/images/transform-images/transform-via-url/ + */ +export async function resizeImageWithCDNCgi( + input: string, + options: CloudflareImageOptions & { + signal?: AbortSignal; + } +): Promise { + const { signal, ...resizeOptions } = options; + + // Since Cloudflare Images options on fetch are not supported on Cloudflare Pages, + // we need to use the Cloudflare Image Resize API directly. + if (!GITBOOK_IMAGE_RESIZE_URL) { + throw new Error('GITBOOK_IMAGE_RESIZE_URL is not set for cdn-cgi image resize mode'); + } + + const resizeURL = `${GITBOOK_IMAGE_RESIZE_URL}${stringifyOptions( + resizeOptions + )}/${encodeURIComponent(input)}`; + + // biome-ignore lint/suspicious/noConsole: this log is useful for debugging + console.log(`resize image using cdn-cgi: ${resizeURL}`); + + return copyImageResponse( + await fetch(resizeURL, { + headers: { + // Pass the `Accept` header, as Cloudflare uses this to validate the format. + Accept: + resizeOptions.format === 'json' + ? 'application/json' + : `image/${resizeOptions.format || 'jpeg'}`, + }, + signal, + }) + ); +} + +function stringifyOptions(options: CloudflareImageOptions): string { + return Object.entries({ ...options }).reduce((rest, [key, value]) => { + return `${rest}${rest ? ',' : ''}${key}=${value}`; + }, ''); +} diff --git a/packages/gitbook-v2/src/lib/images/resizer/cf-fetch.ts b/packages/gitbook-v2/src/lib/images/resizer/cf-fetch.ts new file mode 100644 index 000000000..8032a3bb3 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/cf-fetch.ts @@ -0,0 +1,34 @@ +import type { CloudflareImageOptions } from './types'; +import { copyImageResponse } from './utils'; + +/** + * Resize an image by doing a request to the image itself using the Cloudflare fetch. + * https://developers.cloudflare.com/images/transform-images/transform-via-workers/ + * + * This method doesn't work in Cloudflare Pages and is only supported in workers. + */ +export async function resizeImageWithCFFetch( + input: string, + options: CloudflareImageOptions & { + signal?: AbortSignal; + } +): Promise { + const { signal, ...resizeOptions } = options; + + // biome-ignore lint/suspicious/noConsole: this log is useful for debugging + console.log(`resize image using cf-fetch: ${input}`); + + return copyImageResponse( + await fetch(input, { + headers: { + // Pass the `Accept` header, as Cloudflare uses this to validate the format. + Accept: + resizeOptions.format === 'json' + ? 'application/json' + : `image/${resizeOptions.format || 'jpeg'}`, + }, + signal, + cf: { image: resizeOptions }, + }) + ); +} diff --git a/packages/gitbook-v2/src/lib/images/resizer/index.ts b/packages/gitbook-v2/src/lib/images/resizer/index.ts new file mode 100644 index 000000000..53d3ce097 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './resizeImage'; diff --git a/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts new file mode 100644 index 000000000..8e656f3b7 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/resizeImage.ts @@ -0,0 +1,72 @@ +import 'server-only'; +import assertNever from 'assert-never'; +import { GITBOOK_IMAGE_RESIZE_MODE } from '../../env'; +import { SizableImageAction, checkIsSizableImageURL } from '../checkIsSizableImageURL'; +import { resizeImageWithCDNCgi } from './cdn-cgi'; +import { resizeImageWithCFFetch } from './cf-fetch'; +import type { CloudflareImageJsonFormat, CloudflareImageOptions } from './types'; + +/** + * Get the size of an image. + */ +export async function getImageSize( + input: string, + defaultSize: Partial = {} +): Promise<{ width: number; height: number } | null> { + if (checkIsSizableImageURL(input) !== SizableImageAction.Resize) { + return null; + } + + try { + const response = await resizeImage(input, { + // Abort the request after 2 seconds to avoid blocking rendering for too long + signal: AbortSignal.timeout(2000), + // Measure size and resize it to the most common size + // to optimize caching + ...defaultSize, + format: 'json', + anim: false, + }); + + const json = (await response.json()) as CloudflareImageJsonFormat; + return { + width: json.original.width, + height: json.original.height, + }; + } catch (error) { + console.warn(`Error getting image size for ${input}:`, error); + return null; + } +} + +/** + * Execute a Cloudflare Image Resize operation on an image. + */ +export async function resizeImage( + input: string, + options: CloudflareImageOptions & { + signal?: AbortSignal; + } +): Promise { + const action = checkIsSizableImageURL(input); + if (action === SizableImageAction.Skip) { + throw new Error( + 'Cannot resize this image, this function should have never been called on this url' + ); + } + + if (action === SizableImageAction.Passthrough) { + return fetch(input, { + signal: options.signal, + }); + } + + switch (GITBOOK_IMAGE_RESIZE_MODE) { + case 'cdn-cgi': + return resizeImageWithCDNCgi(input, options); + case 'cf-fetch': + return resizeImageWithCFFetch(input, options); + default: + assertNever(GITBOOK_IMAGE_RESIZE_MODE); + } +} diff --git a/packages/gitbook-v2/src/lib/images/resizer/types.ts b/packages/gitbook-v2/src/lib/images/resizer/types.ts new file mode 100644 index 000000000..8bbe697f9 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/types.ts @@ -0,0 +1,23 @@ +export interface CloudflareImageJsonFormat { + width: number; + height: number; + original: { + file_size: number; + width: number; + height: number; + format: string; + }; +} + +/** + * https://developers.cloudflare.com/images/image-resizing/resize-with-workers/ + */ +export interface CloudflareImageOptions { + format?: 'webp' | 'avif' | 'json' | 'jpeg'; + fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; + width?: number; + height?: number; + dpr?: number; + anim?: boolean; + quality?: number; +} diff --git a/packages/gitbook-v2/src/lib/images/resizer/utils.ts b/packages/gitbook-v2/src/lib/images/resizer/utils.ts new file mode 100644 index 000000000..99dc599c8 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/resizer/utils.ts @@ -0,0 +1,7 @@ +/** + * Copy a response to make sure it can be mutated by the rest of the middleware. + * To avoid errors "Can't modify immutable headers". + */ +export function copyImageResponse(response: Response) { + return new Response(response.body, response); +} diff --git a/packages/gitbook-v2/src/lib/images/signatures.ts b/packages/gitbook-v2/src/lib/images/signatures.ts index 19aafdfa3..21834a69c 100644 --- a/packages/gitbook-v2/src/lib/images/signatures.ts +++ b/packages/gitbook-v2/src/lib/images/signatures.ts @@ -32,6 +32,11 @@ export async function verifyImageSignature( ): Promise { const generator = IMAGE_SIGNATURE_FUNCTIONS[version]; const generated = await generator(input); + + // biome-ignore lint/suspicious/noConsole: we want to log the signature comparison + console.log( + `comparing image signature for "${input.url}" on identifier "${input.imagesContextId}": "${generated}" (expected) === "${signature}" (actual)` + ); return generated === signature; } @@ -65,7 +70,9 @@ const generateSignatureV2: SignFn = async (input) => { ] .filter(Boolean) .join(':'); - return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16); + + const signature = fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16); + return signature; }; // Reused buffer for FNV-1a hashing in the v1 algorithm diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts index 857345703..b84565d1e 100644 --- a/packages/gitbook-v2/src/lib/links.ts +++ b/packages/gitbook-v2/src/lib/links.ts @@ -93,6 +93,11 @@ export function createLinker( }, toAbsoluteURL(absolutePath: string): string { + // If the path is already a full URL, we return it as is. + if (URL.canParse(absolutePath)) { + return absolutePath; + } + if (!servedOn.host) { return absolutePath; } diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index ae5aa4a36..916d53d61 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -10,21 +10,22 @@ import { type ResponseCookies, getPathScopedCookieName, getResponseCookiesForVisitorAuth, - getVisitorToken, + getVisitorData, normalizeVisitorAuthURL, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { serveResizedImage } from '@/routes/image'; import { DataFetcherError, getPublishedContentByURL, + getVisitorAuthBasePath, normalizeURL, + resolvePublishedContentByUrl, throwIfDataError, } from '@v2/lib/data'; import { isGitBookAssetsHostURL, isGitBookHostURL } from '@v2/lib/env'; import { getImageResizingContextId } from '@v2/lib/images'; import { MiddlewareHeaders } from '@v2/lib/middleware'; import type { SiteURLData } from './lib/context'; - export const config = { matcher: [ '/((?!_next/static|_next/image|~gitbook/static|~gitbook/revalidate|~gitbook/monitoring|~scalar/proxy).*)', @@ -33,6 +34,15 @@ export const config = { type URLWithMode = { url: URL; mode: 'url' | 'url-host' }; +/** + * Temporary list of hosts to test adaptive content using the new resolution API. + */ +const ADAPTIVE_CONTENT_HOSTS = [ + 'docs.gitbook.com', + 'adaptive-docs.gitbook-staging.com', + 'enriched-content-playground.gitbook-staging.io', +]; + export async function middleware(request: NextRequest) { try { const requestURL = new URL(request.url); @@ -85,17 +95,22 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // // Detect and extract the visitor authentication token from the request // - // @ts-ignore - request typing - const visitorToken = getVisitorToken({ + const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({ cookies: request.cookies.getAll(), url: siteRequestURL, }); const withAPIToken = async (apiToken: string | null) => { + const resolve = ADAPTIVE_CONTENT_HOSTS.includes(siteRequestURL.hostname) + ? resolvePublishedContentByUrl + : getPublishedContentByURL; const siteURLData = await throwIfDataError( - getPublishedContentByURL({ + resolve({ url: siteRequestURL.toString(), - visitorAuthToken: visitorToken?.token ?? null, + visitorPayload: { + jwtToken: visitorToken?.token ?? undefined, + unsignedClaims, + }, // When the visitor auth token is pulled from the cookie, set redirectOnError when calling getPublishedContentByUrl to allow // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie. // For example when the VA backend signature has changed but the token stored in the cookie is not yet expired. @@ -106,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { apiToken, }) ); - const cookies: ResponseCookies = []; + + const cookies: ResponseCookies = visitorParamsCookie + ? [ + // If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation. + visitorParamsCookie, + ] + : []; // // Handle redirects @@ -137,24 +158,32 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { return NextResponse.redirect(siteURLData.redirect); } - cookies.push(...getResponseCookiesForVisitorAuth(siteURLData.siteBasePath, visitorToken)); + cookies.push( + ...getResponseCookiesForVisitorAuth( + getVisitorAuthBasePath(siteRequestURL, siteURLData), + visitorToken + ) + ); // We use the host/origin from the canonical URL to ensure the links are // correctly generated when the site is proxied. e.g. https://proxy.gitbook.com/site/siteId/... const siteCanonicalURL = new URL(siteURLData.canonicalUrl); + let incomingURL = requestURL; + // For cases where the site is proxied, we use the canonical URL + // as the incoming URL along with all the search params from the request. + if (mode !== 'url') { + incomingURL = siteCanonicalURL; + incomingURL.search = requestURL.search; + } // // Make sure the URL is clean of any va token after a successful lookup // The token is stored in a cookie that is set on the redirect response // - const incomingURL = mode === 'url' ? requestURL : siteCanonicalURL; - const requestURLWithoutToken = normalizeVisitorAuthURL(incomingURL); - if ( - requestURLWithoutToken !== incomingURL && - requestURLWithoutToken.toString() !== incomingURL.toString() - ) { + const incomingURLWithoutToken = normalizeVisitorAuthURL(incomingURL); + if (incomingURLWithoutToken.toString() !== incomingURL.toString()) { return writeResponseCookies( - NextResponse.redirect(requestURLWithoutToken.toString()), + NextResponse.redirect(incomingURLWithoutToken.toString()), cookies ); } @@ -167,6 +196,23 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // (customization override, theme, etc) let routeType: 'dynamic' | 'static' = 'static'; + // We pick only stable data from the siteURL data to prevent re-rendering of + // the root layout when changing pages.. + const stableSiteURLData: SiteURLData = { + site: siteURLData.site, + siteSection: siteURLData.siteSection, + siteSpace: siteURLData.siteSpace, + siteBasePath: siteURLData.siteBasePath, + basePath: siteURLData.basePath, + space: siteURLData.space, + organization: siteURLData.organization, + changeRequest: siteURLData.changeRequest, + revision: siteURLData.revision, + shareKey: siteURLData.shareKey, + apiToken: siteURLData.apiToken, + imagesContextId: imagesContextId, + }; + const requestHeaders = new Headers(request.headers); requestHeaders.set(MiddlewareHeaders.RouteType, routeType); requestHeaders.set(MiddlewareHeaders.URLMode, mode); @@ -174,7 +220,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { MiddlewareHeaders.SiteURL, `${siteCanonicalURL.origin}${siteURLData.basePath}` ); - requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(siteURLData)); + requestHeaders.set(MiddlewareHeaders.SiteURLData, JSON.stringify(stableSiteURLData)); // Preview of customization/theme const customization = siteRequestURL.searchParams.get('customization'); @@ -204,23 +250,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { ); routeType = routeTypeFromPathname ?? routeType; - // We pick only stable data from the siteURL data to prevent re-rendering of - // the root layout when changing pages.. - const stableSiteURLData: SiteURLData = { - site: siteURLData.site, - siteSection: siteURLData.siteSection, - siteSpace: siteURLData.siteSpace, - siteBasePath: siteURLData.siteBasePath, - basePath: siteURLData.basePath, - space: siteURLData.space, - organization: siteURLData.organization, - changeRequest: siteURLData.changeRequest, - revision: siteURLData.revision, - shareKey: siteURLData.shareKey, - apiToken: siteURLData.apiToken, - imagesContextId: imagesContextId, - }; - const route = [ 'sites', routeType, diff --git a/packages/gitbook-v2/wrangler.jsonc b/packages/gitbook-v2/wrangler.jsonc new file mode 100644 index 000000000..6d9c2b232 --- /dev/null +++ b/packages/gitbook-v2/wrangler.jsonc @@ -0,0 +1,156 @@ +{ + "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" + ], + "assets": { + "directory": ".open-next/assets", + "binding": "ASSETS" + }, + "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", + "bucket_name": "gitbook-open-v2-cache-preview" + } + ], + "services": [ + { + "binding": "WORKER_SELF_REFERENCE", + "service": "gitbook-open-v2-preview" + }, + { + "binding": "GITBOOK_API", + "service": "gitbook-x-prod-api-cache" + } + ] + // No durable objects on preview, as they block the generation of preview URLs + // and we don't need tags invalidation on preview + }, + "staging": { + "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": "GITBOOK_API", + "service": "gitbook-x-staging-api-cache" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-staging-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"] + } + ] + }, + "production": { + "vars": { + // This is a bit misleading, but it means that we can have 500 concurrent revalidations + // This means that we'll have up to 100 durable objects instance running at the same time + "MAX_REVALIDATE_CONCURRENCY": "100", + // Temporary variable to find the issue once deployed + // TODO: remove this once the issue is fixed + "DEBUG_CLOUDFLARE": "true" + }, + "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": "GITBOOK_API", + "service": "gitbook-x-prod-api-cache" + } + ], + "tail_consumers": [ + { + "service": "gitbook-x-prod-tail" + } + ], + "durable_objects": { + "bindings": [ + { + "name": "NEXT_CACHE_DO_QUEUE", + "class_name": "DOQueueHandler" + }, + { + "name": "NEXT_TAG_CACHE_DO_SHARDED", + "class_name": "DOShardedTagCache" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"] + } + ] + } + } +} diff --git a/packages/gitbook-v2/wrangler.toml b/packages/gitbook-v2/wrangler.toml deleted file mode 100644 index 86e45433b..000000000 --- a/packages/gitbook-v2/wrangler.toml +++ /dev/null @@ -1,50 +0,0 @@ -main = ".open-next/worker.js" -name = "gitbook-open-v2" -compatibility_date = "2025-03-11" -compatibility_flags = ["nodejs_compat", "allow_importable_env"] -assets = { directory = ".open-next/assets", binding = "ASSETS" } -observability = { enabled = true } - -[env.preview] -kv_namespaces = [ - { binding = "NEXT_CACHE_WORKERS_KV", id = "b7dd9cf58bf2458f84812a2d83b3760c" } # gitbook-open-v2-cache-preview -] -d1_databases = [ - { binding = "NEXT_CACHE_D1", database_id = "f59ddb40-ad72-4312-9395-0ac6a129af8e", database_name = "gitbook-open-v2-tags-preview" } -] -services = [ - { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-preview" }, - { binding = "GITBOOK_API", service = "gitbook-x-prod-api-cache" } -] - -[env.staging] -routes = [ - { pattern = "open-2c.gitbook-staging.com/*", zone_name = "gitbook-staging.com" }, - { pattern = "static-2c.gitbook-staging.com/*", zone_name = "gitbook-staging.com" } -] -kv_namespaces = [ - { binding = "NEXT_CACHE_WORKERS_KV", id = "a446e25f12b741afb185f1e5b4474f0a" } # gitbook-open-v2-cache-staging -] -d1_databases = [ - { binding = "NEXT_CACHE_D1", database_id = "9df62e39-1f35-4066-83aa-e9b8ed3ac8d5", database_name = "gitbook-open-v2-tags-staging" } -] -services = [ - { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-staging" }, - { binding = "GITBOOK_API", service = "gitbook-x-staging-api-cache" } -] - -[env.production] -routes = [ - { pattern = "open-2c.gitbook.com/*", zone_name = "gitbook.com" }, - { pattern = "static-2c.gitbook.com/*", zone_name = "gitbook.com" } -] -kv_namespaces = [ - { binding = "NEXT_CACHE_WORKERS_KV", id = "72379746280d4e79acf24440eea950dc" } # gitbook-open-v2-cache-production -] -d1_databases = [ - { binding = "NEXT_CACHE_D1", database_id = "a6f16fce-5f45-43a9-89a4-7b83ddf25b77", database_name = "gitbook-open-v2-tags-production" } -] -services = [ - { binding = "NEXT_CACHE_REVALIDATION_WORKER", service = "gitbook-open-v2-production" }, - { binding = "GITBOOK_API", service = "gitbook-x-prod-api-cache" } -] diff --git a/packages/gitbook/CHANGELOG.md b/packages/gitbook/CHANGELOG.md index fbb5ed1ae..fb855a630 100644 --- a/packages/gitbook/CHANGELOG.md +++ b/packages/gitbook/CHANGELOG.md @@ -1,5 +1,132 @@ # 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 + +- Updated dependencies [ebc39e9] +- Updated dependencies [b6b09d4] + - @gitbook/react-openapi@1.2.1 + +## 0.11.0 + +### Minor Changes + +- d67699a: Add OpenAPI Webhook block + +### Patch Changes + +- 4b8a621: Show sections tabs only if there is at least two sections +- 8ed1bda: Translate OpenAPI blocks +- 7588cfe: Improve OpenAPIResponses examples and schemas +- Updated dependencies [eeb977f] +- Updated dependencies [3363a18] +- Updated dependencies [d67699a] +- Updated dependencies [8ed1bda] +- Updated dependencies [7588cfe] +- Updated dependencies [ad1dc0b] + - @gitbook/react-openapi@1.2.0 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [77397ca] + - @gitbook/cache-tags@0.3.1 + +## 0.10.0 + +### Minor Changes + +- b62b101: Do not set cookie to identify visitor for insights when disabled. + +### Patch Changes + +- 95ea22d: Cache AI Page Link summary +- daf41fc: Tweak footer design (and refactor) +- de53946: Fix security issue with injection of "javacript:` url in the back button of PDFs +- b92ecfa: Implement retry logic for the DO cache to prevent when revalidating content. +- 528eee3: Add superscript and subscript text rendering +- aa3357a: Fix OpenAPISchemas description padding +- 168a4fa: Add support for buttons to GitBook. +- 70c4182: Improve OpenAPI schema style +- 2b6c593: Remove stable from x-stability +- 580f7ad: Improve the error message returned by the revalidate endpoint. +- cbd768a: Improve OpenAPI codesample (add OpenAPISelect component) +- c765463: Fix ogimage generation crashing when site is using a custom WOFF2 font +- e59076a: Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX. +- 29aaba5: Override Scalar's overscroll-behavior +- 90ead98: Better error handling in cache revalidation. +- Updated dependencies [116575c] +- Updated dependencies [cdffd7c] +- Updated dependencies [70c4182] +- Updated dependencies [2b6c593] +- Updated dependencies [cbd768a] +- Updated dependencies [e59076a] +- Updated dependencies [eedefdd] +- Updated dependencies [23cedd2] + - @gitbook/cache-tags@0.3.0 + - @gitbook/colors@0.3.2 + - @gitbook/react-openapi@1.1.10 + - @gitbook/openapi-parser@2.1.3 + +## 0.9.2 + +### Patch Changes + +- da7b369: Fix missing headers in OpenAPIResponses +- 139a805: Fix OpenAPI enum display +- Updated dependencies [da7b369] +- Updated dependencies [da485f5] +- Updated dependencies [139a805] + - @gitbook/react-openapi@1.1.9 + ## 0.9.1 ### Patch Changes @@ -483,7 +610,7 @@ - 4cbcc5b: Rollback of scalar modal while fixing perf issue - 3996110: Optimize images rendered in community ads - 133c3e7: Update design of Checkbox to be more consistent and readable -- 5096f7f: Disable KV cache for docs.gitbook.com as a test, also disable it for change-request to improve consistency +- 5096f7f: Disable KV cache for gitbook.com/docs as a test, also disable it for change-request to improve consistency - 0f1565c: Add optional env `GITBOOK_INTEGRATIONS_HOST` to configure the host serving the integrations - 2ff7ed1: Fix table of contents being visible on mobile when disabled at the page level - b075f0f: Fix accessibility of the table of contents by using `aria-current` instead of `aria-selected` diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts index 7b3b0ce26..f0612ba8e 100644 --- a/packages/gitbook/e2e/customers.spec.ts +++ b/packages/gitbook/e2e/customers.spec.ts @@ -10,18 +10,18 @@ const testCases: TestsCase[] = [ { name: 'OpenAPI', url: '/snyk-api/reference/apps', run: waitForCookiesDialog }, ], }, - { - name: 'Nexthink', - contentBaseURL: 'https://docs.nexthink.com', - tests: [ - { - name: 'Home', - url: '/', - screenshot: { waitForTOCScrolling: false }, - run: waitForCookiesDialog, - }, - ], - }, + // { + // name: 'Nexthink', + // contentBaseURL: 'https://docs.nexthink.com', + // tests: [ + // { + // name: 'Home', + // url: '/', + // screenshot: { waitForTOCScrolling: false }, + // run: waitForCookiesDialog, + // }, + // ], + // }, { name: 'asiksupport-stg.dto.kemkes.go.id', contentBaseURL: 'https://asiksupport-stg.dto.kemkes.go.id', @@ -92,11 +92,6 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://book.character.ai', tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, - { - name: 'docs.tradeonnova.io', - contentBaseURL: 'https://docs.tradeonnova.io', - tests: [{ name: 'Home', url: '/' }], - }, { name: 'azcoiner.gitbook.io', contentBaseURL: 'https://azcoiner.gitbook.io', @@ -162,11 +157,11 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://wiki.redmodding.org', tests: [{ name: 'Home', url: '/' }], }, - { - name: 'docs.cherry-ai.com', - contentBaseURL: 'https://docs.cherry-ai.com', - tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], - }, + // { + // name: 'docs.cherry-ai.com', + // contentBaseURL: 'https://docs.cherry-ai.com', + // tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], + // }, { name: 'docs.snyk.io', contentBaseURL: 'https://docs.snyk.io', @@ -244,6 +239,15 @@ const testCases: TestsCase[] = [ contentBaseURL: 'https://docs.fluentbit.io', tests: [{ name: 'Home', url: '/', run: waitForCookiesDialog }], }, + { + name: 'run-ai-docs.nvidia.com', + contentBaseURL: 'https://run-ai-docs.nvidia.com', + skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2', + tests: [ + { name: 'Home', url: '/' }, + { name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' }, + ], + }, ]; runTestCases(testCases); diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 4b244a0c4..a70a7e5d8 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -12,7 +12,7 @@ import { VISITOR_TOKEN_COOKIE, getVisitorAuthCookieName, getVisitorAuthCookieValue, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { getSiteAPIToken } from '../tests/utils'; import { @@ -111,24 +111,24 @@ const testCases: TestsCase[] = [ name: 'Customized variant titles are displayed', url: '', run: async (page) => { - const spaceDrowpdown = page + const spaceDropdown = page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); const variantSelectionDropdown = page.locator( - 'css=[data-testid="space-dropdown-button"] + div' + 'css=[data-testid="dropdown-menu"]' ); // the customized space title await expect( - variantSelectionDropdown.getByRole('link', { + variantSelectionDropdown.getByRole('menuitem', { name: 'Multi-Variants', }) ).toBeVisible(); // the NON-customized space title await expect( - variantSelectionDropdown.getByRole('link', { + variantSelectionDropdown.getByRole('menuitem', { name: 'RFCs', }) ).toBeVisible(); @@ -145,14 +145,17 @@ const testCases: TestsCase[] = [ url: 'api-multi-versions/reference/api-reference/pets', screenshot: false, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); @@ -168,14 +171,18 @@ const testCases: TestsCase[] = [ url: 'api-multi-versions-share-links/8tNo6MeXg7CkFMzSSz81/reference/api-reference/pets', screenshot: false, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); @@ -205,14 +212,18 @@ const testCases: TestsCase[] = [ return `api-multi-versions-va/reference/api-reference/pets?jwt_token=${token}`; }, run: async (page) => { - const spaceDrowpdown = await page + const spaceDropdown = await page .locator('[data-testid="space-dropdown-button"]') .locator('visible=true'); - await spaceDrowpdown.click(); + await spaceDropdown.click(); + + const variantSelectionDropdown = page.locator( + 'css=[data-testid="dropdown-menu"]' + ); // Click the second variant in the dropdown - await page - .getByRole('link', { + await variantSelectionDropdown + .getByRole('menuitem', { name: '2.0', }) .click(); @@ -258,7 +269,7 @@ const testCases: TestsCase[] = [ }, { name: 'GitBook', - contentBaseURL: 'https://docs.gitbook.com', + contentBaseURL: 'https://gitbook.com/docs/', tests: [ { name: 'Home', @@ -419,6 +430,43 @@ const testCases: TestsCase[] = [ }, ], }, + { + name: 'Site subdirectory (proxy)', + skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel', + contentBaseURL: 'https://nextjs-gbo-proxy.vercel.app/documentation/', + tests: [ + { + name: 'Main', + url: '', + fullPage: true, + }, + ], + }, + { + name: 'Site subdirectory (proxy) with VA', + skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel', + contentBaseURL: 'https://nextjs-gbo-proxy-va.vercel.app/va/docs/', + tests: [ + { + name: 'Main', + url: () => { + const privateKey = + 'rqSfA6x7eAKx1qDRCDq9aCXwivpUvQ8YkXeDdFvCCUa9QchIcM7pF1iJ4o7AGOU49spmOWjKoIPtX0pVUVQ81w=='; + const token = jwt.sign( + { + name: 'gitbook-open-tests', + }, + privateKey, + { + expiresIn: '24h', + } + ); + return `?jwt_token=${token}`; + }, + fullPage: true, + }, + ], + }, { name: 'Content tests', contentBaseURL: 'https://gitbook.gitbook.io/test-gitbook-open/', @@ -837,6 +885,20 @@ const testCases: TestsCase[] = [ }, ], }, + { + name: 'Content Redirects', + contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/', + tests: [ + { + name: 'Redirect to new location', + url: '/content-editor/editing-content/inline/redirect-test', + run: async (page) => { + await expect(page.locator('h1')).toHaveText('Redirect test'); + }, + screenshot: false, + }, + ], + }, { name: 'Site Redirects with sections', contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/sections/', diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index 566daa4a9..61113f826 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -34,6 +34,10 @@ export interface Test { * Test to run */ run?: (page: Page, response: Response | null) => Promise; + /** + * Mode for the test. + */ + mode?: 'page' | 'image'; /** * Whether the test should be fullscreened during testing. */ @@ -154,6 +158,7 @@ export function runTestCases(testCases: TestsCase[]) { test.describe(testCase.name, () => { for (const testEntry of testCase.tests) { + const { mode = 'page' } = testEntry; const testFn = testEntry.only ? test.only : test; testFn(testEntry.name, async ({ page, context }) => { const testEntryPathname = @@ -163,6 +168,7 @@ export function runTestCases(testCases: TestsCase[]) { new URL(testEntryPathname, testCase.contentBaseURL).toString() ) : getTestURL(testEntryPathname); + if (testEntry.cookies) { await context.addCookies( testEntry.cookies.map((cookie) => ({ @@ -194,24 +200,33 @@ export function runTestCases(testCases: TestsCase[]) { } const screenshotOptions = testEntry.screenshot; if (screenshotOptions !== false) { - await argosScreenshot(page, `${testCase.name} - ${testEntry.name}`, { - viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'], - argosCSS: ` + const screenshotName = `${testCase.name} - ${testEntry.name}`; + if (mode === 'image') { + await argosScreenshot(page, screenshotName, { + viewports: ['macbook-13'], + threshold: screenshotOptions?.threshold ?? undefined, + fullPage: true, + }); + } else { + await argosScreenshot(page, screenshotName, { + viewports: ['macbook-16', 'macbook-13', 'ipad-2', 'iphone-x'], + argosCSS: ` /* Hide Intercom */ .intercom-lightweight-app { display: none !important; } `, - threshold: screenshotOptions?.threshold ?? undefined, - fullPage: testEntry.fullPage ?? false, - beforeScreenshot: async ({ runStabilization }) => { - await runStabilization(); - await waitForIcons(page); - if (screenshotOptions?.waitForTOCScrolling !== false) { - await waitForTOCScrolling(page); - } - }, - }); + threshold: screenshotOptions?.threshold ?? undefined, + fullPage: testEntry.fullPage ?? false, + beforeScreenshot: async ({ runStabilization }) => { + await runStabilization(); + if (screenshotOptions?.waitForTOCScrolling !== false) { + await waitForTOCScrolling(page); + } + await waitForIcons(page); + }, + }); + } } }); } @@ -276,6 +291,9 @@ export function getCustomizationURL(partial: DeepPartial { - const urls = new Set(); + const urlStates: Record< + string, + { state: 'pending'; uri: null } | { state: 'loaded'; uri: string } + > = (window as any).__ICONS_STATES__ || {}; + (window as any).__ICONS_STATES__ = urlStates; + + const fetchSvgAsDataUri = async (url: string): Promise => { + 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 }; + }); + }; + const icons = Array.from(document.querySelectorAll('svg.gb-icon')); const results = icons.map((icon) => { if (!(icon instanceof SVGElement)) { throw new Error('Icon is not an SVGElement'); } - // If loaded, good it passes the test. - if (icon.dataset.loadingState === 'loaded') { + // Ignore icons that are not visible. + if (!icon.checkVisibility()) { return true; } - // If not loaded yet, we need to load it. - if (icon.dataset.loadingState === 'pending') { + const state = icon.getAttribute('data-argos-state'); + + if (state === 'pending') { return false; } - // Ignore icons that are not visible. - if (!icon.checkVisibility()) { + if (state === 'loaded') { return true; } // url("https://ka-p.fontawesome.com/releases/v6.6.0/svgs/light/moon.svg?v=2&token=a463935e93") const maskImage = window.getComputedStyle(icon).getPropertyValue('mask-image'); const urlMatch = maskImage.match(/url\("([^"]+)"\)/); - const url = urlMatch ? urlMatch[1] : null; + const url = urlMatch?.[1]; // If URL is invalid we throw an error. if (!url) { throw new Error('No mask-image'); } - // If the URL is already loaded, we just mark it as loaded. - if (urls.has(url)) { - icon.dataset.loadingState = 'loaded'; - return true; - } - - // Mark the icon as pending and load the image. - icon.dataset.loadingState = 'pending'; - - // Mark the URL as seen. - urls.add(url); - - const img = new Image(); - img.src = url; - img.decode().then(() => { - // Wait two frames to let the time to the icon to repaint. - requestAnimationFrame(() => { + // If the URL is already queued for loading, we return the state. + if (urlStates[url]) { + if (urlStates[url].state === 'loaded') { + icon.setAttribute('data-argos-state', 'pending'); + icon.style.maskImage = `url("${urlStates[url].uri}")`; requestAnimationFrame(() => { - icon.dataset.loadingState = 'loaded'; + icon.setAttribute('data-argos-state', 'loaded'); }); - }); - }); + return false; + } + + return false; + } + loadUrl(url); return false; }); diff --git a/packages/gitbook/next.config.js b/packages/gitbook/next.config.js index 8aec4f2e7..23c575ec1 100644 --- a/packages/gitbook/next.config.js +++ b/packages/gitbook/next.config.js @@ -5,6 +5,7 @@ module.exports = { GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL, GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, + GITBOOK_RUNTIME: process.env.GITBOOK_RUNTIME, }, webpack(config) { diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 58b07756a..a3b9da821 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -1,6 +1,6 @@ { "name": "gitbook", - "version": "0.9.1", + "version": "0.12.0", "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": "*", + "@gitbook/api": "^0.115.0", "@gitbook/cache-do": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", @@ -27,17 +27,18 @@ "@gitbook/react-math": "workspace:*", "@gitbook/react-openapi": "workspace:*", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dropdown-menu": "^2.1.12", "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-tooltip": "^1.1.8", "@sindresorhus/fnv1a": "^3.1.0", "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.16", - "@upstash/redis": "^1.27.1", - "ai": "^4.1.46", - "ajv": "^8.12.0", + "ai": "^4.2.2", "assert-never": "^1.2.1", "bun-types": "^1.1.20", "classnames": "^2.5.1", + "event-iterator": "^2.0.0", "framer-motion": "^10.16.14", "js-cookie": "^3.0.5", "jsontoxml": "^1.0.1", @@ -46,15 +47,16 @@ "mathjax": "^3.2.2", "mdast-util-to-markdown": "^2.1.2", "memoizee": "^0.4.17", - "next": "14.2.25", + "next": "14.2.26", "next-themes": "^0.2.1", "nuqs": "^2.2.3", "object-hash": "^3.0.0", "openapi-types": "^12.1.3", "p-map": "^7.0.0", "parse-cache-control": "^1.0.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "partial-json": "^0.1.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "react-hotkeys-hook": "^4.4.1", "rehype-sanitize": "^6.0.0", "rehype-stringify": "^10.0.1", @@ -68,11 +70,14 @@ "tailwind-shades": "^1.1.2", "unified": "^11.0.5", "url-join": "^5.0.0", - "usehooks-ts": "^3.1.0" + "usehooks-ts": "^3.1.0", + "zod": "^3.24.2", + "zod-to-json-schema": "^3.24.5", + "zustand": "^5.0.3" }, "devDependencies": { - "@argos-ci/playwright": "^4.3.0", - "@cloudflare/next-on-pages": "1.13.7", + "@argos-ci/playwright": "^5.0.3", + "@cloudflare/next-on-pages": "1.13.12", "@cloudflare/workers-types": "^4.20241230.0", "@playwright/test": "^1.51.1", "@types/js-cookie": "^3.0.6", diff --git a/packages/gitbook/public/_headers b/packages/gitbook/public/_headers index 193088b36..0d7eadbf4 100644 --- a/packages/gitbook/public/_headers +++ b/packages/gitbook/public/_headers @@ -1,3 +1,7 @@ # GitBook immutable static assets +# Duplicated from next.config.mjs until OpenNext supports generating static headers /~gitbook/static/* cache-control: public,max-age=31536000,immutable + Access-Control-Allow-Origin: * +/_next/static/* + Access-Control-Allow-Origin: * diff --git a/packages/gitbook/src/app/(global)/~gitbook/revalidate/route.ts b/packages/gitbook/src/app/(global)/~gitbook/revalidate/route.ts index 91c214e12..95ef8e9dc 100644 --- a/packages/gitbook/src/app/(global)/~gitbook/revalidate/route.ts +++ b/packages/gitbook/src/app/(global)/~gitbook/revalidate/route.ts @@ -14,7 +14,15 @@ interface JsonBody { * The body should be a JSON with { tags: string[] } */ export async function POST(req: NextRequest) { - const json = (await req.json()) as JsonBody; + let json: JsonBody; + + try { + json = await req.json(); + } catch (err) { + return NextResponse.json({ + error: `invalid json body: ${err}`, + }); + } if (!json.tags || !Array.isArray(json.tags)) { return NextResponse.json( @@ -25,10 +33,18 @@ export async function POST(req: NextRequest) { ); } - const result = await revalidateTags(json.tags); - - return NextResponse.json({ - success: true, - stats: result.stats, - }); + try { + const result = await revalidateTags(json.tags); + return NextResponse.json({ + success: true, + stats: result.stats, + }); + } catch (err: unknown) { + return NextResponse.json( + { + error: `${err}`, + }, + { status: 500 } + ); + } } diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx index b9febb51a..27066312c 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/PageClientLayout.tsx @@ -11,7 +11,7 @@ import { useScrollPage } from '@/components/hooks'; export function PageClientLayout(props: { withSections?: boolean }) { // We use this hook in the page layout to ensure the elements for the blocks // are rendered before we scroll to a hash or to the top of the page - useScrollPage({ scrollMarginTop: props.withSections ? 50 : undefined }); + useScrollPage({ scrollMarginTop: props.withSections ? 48 : undefined }); useStripFallbackQueryParam(); return null; diff --git a/packages/gitbook/src/app/middleware/(site)/error.tsx b/packages/gitbook/src/app/middleware/(site)/error.tsx index 4f96f4e65..dc89d8547 100644 --- a/packages/gitbook/src/app/middleware/(site)/error.tsx +++ b/packages/gitbook/src/app/middleware/(site)/error.tsx @@ -1,7 +1,7 @@ 'use client'; import { Button } from '@/components/primitives/Button'; -import { t, useLanguage } from '@/intl/client'; +import { t, tString, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; export default function ErrorPage(props: { @@ -35,9 +35,8 @@ export default function ErrorPage(props: { }} variant="secondary" size="small" - > - {t(language, 'unexpected_error_retry')} - + label={tString(language, 'unexpected_error_retry')} + /> diff --git a/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx b/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx new file mode 100644 index 000000000..00087b2a3 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/AIPageLinkSummary.tsx @@ -0,0 +1,190 @@ +'use client'; +import { useLanguage } from '@/intl/client'; +import { t } from '@/intl/translate'; +import { Icon } from '@gitbook/icons'; +import { useEffect } from 'react'; +import { create } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; +import { useVisitedPages } from '../Insights'; +import { usePageContext } from '../PageContext'; +import { Loading } from '../primitives'; +import { streamLinkPageSummary } from './server-actions/streamLinkPageSummary'; + +/** + * Get a unique cache key for a page summary + */ +function getCacheKey(targetSpaceId: string, targetPageId: string): string { + return `${targetSpaceId}:${targetPageId}`; +} + +/** + * Global state for the summaries. + */ +const useSummaries = create<{ + /** + * Cache of all summaries generated so far. + */ + cache: Map; + + /** + * Get a summary for a page. + */ + getSummary: (params: { targetSpaceId: string; targetPageId: string }) => string; + + /** + * Stream the generation of a summary for a page. + */ + streamSummary: (params: { + currentSpaceId: string; + currentPageId: string; + currentPageTitle: string; + targetSpaceId: string; + targetPageId: string; + linkPreview?: string; + linkTitle?: string; + visitedPages: { spaceId: string; pageId: string }[]; + }) => Promise; +}>((set, get) => ({ + cache: new Map(), + + getSummary: ({ + targetSpaceId, + targetPageId, + }: { + targetSpaceId: string; + targetPageId: string; + }) => { + return get().cache.get(getCacheKey(targetSpaceId, targetPageId)) ?? ''; + }, + + streamSummary: async ({ + currentSpaceId, + currentPageId, + currentPageTitle, + targetSpaceId, + targetPageId, + linkPreview, + linkTitle, + visitedPages, + }) => { + const cacheKey = getCacheKey(targetSpaceId, targetPageId); + + if (get().cache.has(cacheKey)) { + // Already generated or generating + return; + } + + const update = (summary: string) => { + set((prev) => { + const newCache = new Map(prev.cache); + newCache.set(cacheKey, summary); + return { cache: newCache }; + }); + }; + + update(''); + const stream = await streamLinkPageSummary({ + currentSpaceId, + currentPageId, + currentPageTitle, + targetSpaceId, + targetPageId, + linkPreview, + linkTitle, + visitedPages, + }); + + let generatedSummary = ''; + for await (const highlight of stream) { + generatedSummary = highlight ?? ''; + update(generatedSummary); + } + }, +})); + +/** + * Summarise a page's content for use in a link preview + */ +export function AIPageLinkSummary(props: { + targetSpaceId: string; + targetPageId: string; + linkPreview?: string; + linkTitle?: string; + showTrademark: boolean; +}) { + const { targetSpaceId, targetPageId, linkPreview, linkTitle, showTrademark = true } = props; + + const currentPage = usePageContext(); + const language = useLanguage(); + const visitedPages = useVisitedPages((state) => state.pages); + const { summary, streamSummary } = useSummaries( + useShallow((state) => { + return { + summary: state.getSummary({ targetSpaceId, targetPageId }), + streamSummary: state.streamSummary, + }; + }) + ); + + useEffect(() => { + streamSummary({ + currentSpaceId: currentPage.spaceId, + currentPageId: currentPage.pageId, + currentPageTitle: currentPage.title, + targetSpaceId, + targetPageId, + linkPreview, + linkTitle, + visitedPages, + }); + }, [ + currentPage.pageId, + currentPage.spaceId, + currentPage.title, + targetSpaceId, + targetPageId, + linkPreview, + linkTitle, + visitedPages, + streamSummary, + ]); + + const shimmerBlocks = [ + 'w-[20%] [animation-delay:-1s]', + 'w-[35%] [animation-delay:-0.8s]', + 'w-[25%] [animation-delay:-0.6s]', + 'w-[10%] [animation-delay:-0.4s]', + 'w-[40%] [animation-delay:-0.2s]', + 'w-[30%] [animation-delay:0s]', + ]; + + return ( +

+
+ {showTrademark ? ( + + ) : ( + + )} +
{t(language, 'link_tooltip_ai_summary')}
+
+ {summary.length > 0 ? ( +

{summary}

+ ) : ( +
+ {shimmerBlocks.map((block, index) => ( +
+ ))} +
+ )} + {summary.length > 0 ? ( +
+ {t(language, 'link_tooltip_ai_summary_description')} +
+ ) : null} +
+ ); +} diff --git a/packages/gitbook/src/components/Adaptive/index.ts b/packages/gitbook/src/components/Adaptive/index.ts new file mode 100644 index 000000000..2d93029d7 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/index.ts @@ -0,0 +1 @@ +export * from './AIPageLinkSummary'; diff --git a/packages/gitbook/src/components/Adaptive/server-actions/api.ts b/packages/gitbook/src/components/Adaptive/server-actions/api.ts new file mode 100644 index 000000000..a1396987d --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/api.ts @@ -0,0 +1,124 @@ +'use server'; +import { type AIMessageInput, AIModel, type AIStreamResponse } 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( + promise: MaybePromise<{ + stream: EventIterator; + 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( + context: GitBookBaseContext, + { + organizationId, + siteId, + }: { + organizationId: string; + siteId: string; + }, + { + schema, + messages, + model = AIModel.Fast, + }: { + schema: z.ZodSchema; + messages: AIMessageInput[]; + model?: AIModel; + previousResponseId?: string; + } +) { + const rawStream = context.dataFetcher.streamAIResponse({ + organizationId, + siteId, + input: messages, + output: { + type: 'object', + schema: zodToJsonSchema(schema), + }, + model, + }); + + let json = ''; + return parseResponse>(rawStream, (event) => { + if (event.type === 'response_object') { + json += event.jsonChunk; + + const parsed = partialJson.parse(json, partialJson.ALL); + return parsed; + } + }); +} + +/** + * Parse a stream from the API to extract the responseId. + */ +function parseResponse( + responseStream: EventIterator, + parse: (response: AIStreamResponse) => T | undefined +): { + stream: EventIterator; + response: Promise<{ responseId: string }>; +} { + let resolveResponse: (value: { responseId: string }) => void; + const response = new Promise<{ responseId: string }>((resolve) => { + resolveResponse = resolve; + }); + + const stream = new EventIterator((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 }; +} diff --git a/packages/gitbook/src/components/Adaptive/server-actions/index.ts b/packages/gitbook/src/components/Adaptive/server-actions/index.ts new file mode 100644 index 000000000..664e869e2 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/index.ts @@ -0,0 +1 @@ +export * from './streamLinkPageSummary'; diff --git a/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts new file mode 100644 index 000000000..88abfe019 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/server-actions/streamLinkPageSummary.ts @@ -0,0 +1,166 @@ +'use server'; +import { filterOutNullable } from '@/lib/typescript'; +import { getV1BaseContext } from '@/lib/v1'; +import { isV2 } from '@/lib/v2'; +import { AIMessageRole } from '@gitbook/api'; +import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; +import { 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* streamLinkPageSummary({ + currentSpaceId, + currentPageId, + targetSpaceId, + targetPageId, + linkPreview, + linkTitle, + visitedPages, +}: { + currentSpaceId: string; + currentPageId: string; + currentPageTitle: string; + targetSpaceId: string; + targetPageId: string; + linkPreview?: string; + linkTitle?: string; + visitedPages?: Array<{ spaceId: string; pageId: string }>; +}) { + const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const siteURLData = await getSiteURLDataFromMiddleware(); + + 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 +Extract a contextually-relevant fact that: +- Directly answers the specific need or question implied by the link's placement +- States a capability, limitation, or specification from the target page +- Connects precisely to the user's current paragraph or sentence +- Completes the user's understanding based on what they're currently reading + +# 3. Instructions +1. First, identify the exact need, question, or gap in the current paragraph where the link appears +2. Find the specific fact in the target page that addresses this exact contextual need +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 +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 +The content of the target page is:`, + 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 +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 +- ALWAYS focus on the gap in knowledge that the link is meant to fill +- ALWAYS consider user's navigation history to ensure contextual continuity +- ALWAYS use action verbs like "click", "select", "use", "create", "enable" + +NEVER: +- NEVER include ANY unspecifc language like "learn", "how to", "discover", etc. State the fact directly. +- NEVER select general facts unrelated to the specific link context +- NEVER ignore the specific context where the link appears +- NEVER repeat the same fact in different words + +## Examples +Current paragraph: "When organizing content, headings are limited to 3 levels. For more advanced editing, you can use (multiple select)[/multiple-select] to move multiple blocks at once." +Preview: "Multiple Select: Select multiple content blocks at once." +✓ "Shift selects content between two points, useful for reorganizing your current heading structure." +✗ "Shift and Ctrl/Cmd keys are the modifiers for selecting multiple blocks." + +Current paragraph: "Most changes can be published directly, but for major revisions, if you want others to review changes before publishing, create a (change request)[/change-requests]." +Preview: "Change Requests: Collaborative content editing workflow." +✓ "Each reviewer's approval is tracked separately, with specific change highlighting for your major revisions." +✗ "Each reviewer receives an email notification and can approve or request changes." + +Current paragraph: "Your team mentioned issues with conflicting edits. Need to collaborate in real-time? You can use (live edit mode)[/live-edit]." +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), + } + ); + + for await (const value of stream) { + const highlight = value.highlight; + if (!highlight) { + continue; + } + + yield highlight; + } +} diff --git a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx index c89bb2480..522bde0ca 100644 --- a/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx +++ b/packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx @@ -25,7 +25,7 @@ export function AnnouncementBanner(props: { const style = BANNER_STYLES[announcement.style]; return ( -
+
{ onUpdateState(true); }} - > - {t(language, 'cookies_accept')} - + label={tString(language, 'cookies_accept')} + /> + label={tString(language, 'cookies_reject')} + />
); diff --git a/packages/gitbook/src/components/DocumentView/Block.tsx b/packages/gitbook/src/components/DocumentView/Block.tsx index f3acafce0..60998b50b 100644 --- a/packages/gitbook/src/components/DocumentView/Block.tsx +++ b/packages/gitbook/src/components/DocumentView/Block.tsx @@ -25,7 +25,7 @@ import { IntegrationBlock } from './Integration'; import { List } from './List'; import { ListItem } from './ListItem'; import { BlockMath } from './Math'; -import { OpenAPIOperation, OpenAPISchemas } from './OpenAPI'; +import { OpenAPIOperation, OpenAPISchemas, OpenAPIWebhook } from './OpenAPI'; import { Paragraph } from './Paragraph'; import { Quote } from './Quote'; import { ReusableContent } from './ReusableContent'; @@ -85,6 +85,8 @@ export function Block(props: BlockProps) { return ; case 'openapi-schemas': return ; + case 'openapi-webhook': + return ; case 'embed': return ; case 'blockquote': @@ -159,6 +161,7 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue } case 'swagger': case 'openapi-operation': case 'openapi-schemas': + case 'openapi-webhook': case 'math': case 'divider': case 'content-ref': diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx index 77d9b33c5..ede23ee1d 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx @@ -26,6 +26,7 @@ export function ClientCodeBlock(props: ClientBlockProps) { const [isInViewport, setIsInViewport] = useState(false); const plainLines = useMemo(() => plainHighlight(block, []), [block]); const [lines, setLines] = useState(null); + const [highlighting, setHighlighting] = useState(false); // Preload the highlighter when the block is mounted. useEffect(() => { @@ -77,6 +78,7 @@ export function ClientCodeBlock(props: ClientBlockProps) { let cancelled = false; if (typeof window !== 'undefined') { + setHighlighting(true); import('./highlight').then(({ highlight }) => { highlight(block, inlines).then((lines) => { if (cancelled) { @@ -84,6 +86,7 @@ export function ClientCodeBlock(props: ClientBlockProps) { } setLines(lines); + setHighlighting(false); }); }); } @@ -98,6 +101,12 @@ export function ClientCodeBlock(props: ClientBlockProps) { }, [isInViewport, block, inlines]); return ( - + ); } diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css index 6a5680179..71d75354e 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.css @@ -24,7 +24,7 @@ } .highlight-line-number { - @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint to-transparent; + @apply text-sm text-right pr-3.5 rounded-l pl-2 sticky left-[-3px] bg-gradient-to-r from-80% from-tint-subtle contrast-more:from-tint-base theme-muted:from-tint-base [html.theme-bold.sidebar-filled_&]:from-tint-base to-transparent; @apply before:text-tint before:content-[counter(line)]; .highlight-line.highlighted > & { diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx index e2279f744..0ad885a44 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlockRenderer.tsx @@ -14,6 +14,7 @@ import './CodeBlockRenderer.css'; type CodeBlockRendererProps = Pick, 'block' | 'style'> & { lines: HighlightLine[]; + 'aria-busy'?: boolean; }; /** @@ -23,7 +24,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( props: CodeBlockRendererProps, ref: React.ForwardedRef ) { - const { block, style, lines } = props; + const { block, style, lines, 'aria-busy': ariaBusy } = props; const id = useId(); const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1; @@ -31,10 +32,14 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( const title = block.data.title; return ( -
+
{title ? ( -
+
{title}
) : null} @@ -45,15 +50,15 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer( />
                 
diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
index 22ce3b60e..5e4a73a88 100644
--- a/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
+++ b/packages/gitbook/src/components/DocumentView/CodeBlock/theme.css
@@ -1,31 +1,67 @@
 :root {
     --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #0a6355;
-    --shiki-token-string: #8b6d32;
-    --shiki-token-comment: theme("colors.teal.700/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.600");
-    --shiki-token-parameter: #0a3069;
-    --shiki-token-function: #8250df;
-    --shiki-token-string-expression: #6a4906;
-    --shiki-token-punctuation: theme("colors.pomegranate.700/.92");
-    --shiki-token-link: theme("colors.tint.12");
-    --shiki-token-inserted: #22863a;
-    --shiki-token-deleted: #b31d28;
-    --shiki-token-changed: #8250df;
+    --shiki-token-punctuation: theme("colors.tint.11");
+    --shiki-token-comment: theme("colors.neutral.9/.7");
+    --shiki-token-link: theme("colors.primary.10");
+
+    --shiki-token-constant: theme("colors.warning.10");
+    --shiki-token-string: theme("colors.warning.10");
+    --shiki-token-string-expression: theme("colors.success.10");
+    --shiki-token-keyword: theme("colors.danger.10");
+    --shiki-token-parameter: theme("colors.warning.10");
+    --shiki-token-function: theme("colors.primary.10");
+
+    --shiki-token-inserted: theme("colors.success.10");
+    --shiki-token-deleted: theme("colors.danger.10");
+    --shiki-token-changed: theme("colors.tint.12");
+}
+
+@media (prefers-contrast: more) {
+    :root {
+        --shiki-color-text: theme("colors.tint.12");
+        --shiki-token-punctuation: theme("colors.tint.12");
+        --shiki-token-comment: theme("colors.neutral.11");
+        --shiki-token-link: theme("colors.primary.11");
+
+        --shiki-token-constant: theme("colors.warning.11");
+        --shiki-token-string: theme("colors.warning.11");
+        --shiki-token-string-expression: theme("colors.success.11");
+        --shiki-token-keyword: theme("colors.danger.11");
+        --shiki-token-parameter: theme("colors.warning.11");
+        --shiki-token-function: theme("colors.primary.11");
+
+        --shiki-token-inserted: theme("colors.success.11");
+        --shiki-token-deleted: theme("colors.danger.11");
+        --shiki-token-changed: theme("colors.tint.12");
+    }
 }
 
 html.dark {
-    --shiki-color-text: theme("colors.tint.11");
-    --shiki-token-constant: #d19a66;
-    --shiki-token-string: theme("colors.pomegranate.300");
-    --shiki-token-comment: theme("colors.teal.300/.64");
-    --shiki-token-keyword: theme("colors.pomegranate.400");
-    --shiki-token-parameter: theme("colors.yellow.500");
-    --shiki-token-function: #56b6c2;
-    --shiki-token-string-expression: theme("colors.tint.11");
-    --shiki-token-punctuation: #acc6ee;
-    --shiki-token-link: theme("colors.pomegranate.400");
-    --shiki-token-inserted: #85e89d;
-    --shiki-token-deleted: #fdaeb7;
-    --shiki-token-changed: #56b6c2;
+    /* Override select colors to have more contrast */
+    --shiki-token-comment: theme("colors.neutral.9");
+
+    --shiki-token-constant: theme("colors.warning.11");
+    --shiki-token-string: theme("colors.warning.11");
+    --shiki-token-string-expression: theme("colors.success.11");
+    --shiki-token-keyword: theme("colors.danger.11");
+    --shiki-token-parameter: theme("colors.warning.11");
+    --shiki-token-function: theme("colors.primary.11");
+}
+
+.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.10");
+    --shiki-token-parameter: theme("colors.tint.9");
+    --shiki-token-function: theme("colors.primary.9");
+}
+
+html.dark.code-monochrome {
+    --shiki-token-constant: theme("colors.tint.11");
+    --shiki-token-string: theme("colors.tint.12");
+    --shiki-token-string-expression: theme("colors.tint.12");
+    --shiki-token-keyword: theme("colors.primary.11");
+    --shiki-token-parameter: theme("colors.tint.10");
+    --shiki-token-function: theme("colors.primary.10");
 }
diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx
index 796cdaeee..17849c195 100644
--- a/packages/gitbook/src/components/DocumentView/Embed.tsx
+++ b/packages/gitbook/src/components/DocumentView/Embed.tsx
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
 import { tcls } from '@/lib/tailwind';
 
 import { getDataOrNull } from '@v2/lib/data';
+import { Image } from '../utils';
 import type { BlockProps } from './Block';
 import { Caption } from './Caption';
 import { IntegrationBlock } from './Integration';
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps) {
                 
+                            Logo
                         ) : null
                     }
                     href={block.data.url}
diff --git a/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
new file mode 100644
index 000000000..02f528c45
--- /dev/null
+++ b/packages/gitbook/src/components/DocumentView/HashLinkButton.tsx
@@ -0,0 +1,58 @@
+import { type ClassValue, tcls } from '@/lib/tailwind';
+import type { DocumentBlockHeading, DocumentBlockTabs } from '@gitbook/api';
+import { Icon } from '@gitbook/icons';
+import { getBlockTextStyle } from './spacing';
+
+/**
+ * A hash icon which adds the block or active block item's ID in the URL hash.
+ * The button needs to be wrapped in a container with `hashLinkButtonWrapperStyles`.
+ */
+export const hashLinkButtonWrapperStyles = tcls('relative', 'group/hash');
+
+export function HashLinkButton(props: {
+    id: string;
+    block: DocumentBlockTabs | DocumentBlockHeading;
+    label?: string;
+    className?: ClassValue;
+    iconClassName?: ClassValue;
+}) {
+    const { id, block, className, iconClassName, label = 'Direct link to block' } = props;
+    const textStyle = getBlockTextStyle(block);
+    return (
+        
+ + + +
+ ); +} diff --git a/packages/gitbook/src/components/DocumentView/Heading.tsx b/packages/gitbook/src/components/DocumentView/Heading.tsx index 8016b0f1b..0de49e623 100644 --- a/packages/gitbook/src/components/DocumentView/Heading.tsx +++ b/packages/gitbook/src/components/DocumentView/Heading.tsx @@ -1,9 +1,9 @@ import type { DocumentBlockHeading } from '@gitbook/api'; -import { Icon } from '@gitbook/icons'; import { tcls } from '@/lib/tailwind'; import type { BlockProps } from './Block'; +import { HashLinkButton, hashLinkButtonWrapperStyles } from './HashLinkButton'; import { Inlines } from './Inlines'; import { getBlockTextStyle } from './spacing'; @@ -20,45 +20,23 @@ export function Heading(props: BlockProps) { return ( -
- - - -
+ +
(props: InlineProps) { const { inline, ...contextProps } = props; @@ -61,6 +64,8 @@ export function Inline< return ; case 'inline-image': return ; + case 'button': + return ; default: assertNever(inline); } diff --git a/packages/gitbook/src/components/DocumentView/InlineButton.tsx b/packages/gitbook/src/components/DocumentView/InlineButton.tsx new file mode 100644 index 000000000..9841c0262 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineButton.tsx @@ -0,0 +1,34 @@ +import { resolveContentRef } from '@/lib/references'; +import * as api from '@gitbook/api'; +import { Button } from '../primitives'; +import type { InlineProps } from './Inline'; + +export async function InlineButton(props: InlineProps) { + const { inline, context } = props; + + if (!context.contentContext) { + throw new Error('InlineButton requires a contentContext'); + } + + const resolved = await resolveContentRef(inline.data.ref, context.contentContext); + + if (!resolved) { + return null; + } + + return ( +
+ {resolved.subText ? ( +

{resolved.subText}

+ ) : null} +
+ + {hasAISummary && 'page' in context && 'page' in inline.data.ref ? ( +
+ +
+ ) : null} +
+ + + + + + ); +} diff --git a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx index fb692c682..155077ca2 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/contentkit.tsx @@ -20,6 +20,8 @@ export const contentKitServerContext: ContentKitServerContext = { 'link-external': (props) => , eye: (props) => , lock: (props) => , + check: (props) => , + 'check-circle': (props) => , }, codeBlock: (props) => { return ; diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx index 294dd876c..1fe281a75 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIOperation.tsx @@ -1,18 +1,14 @@ -import type { JSONDocument } from '@gitbook/api'; -import { Icon } from '@gitbook/icons'; import { OpenAPIOperation as BaseOpenAPIOperation } from '@gitbook/react-openapi'; import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock'; import { tcls } from '@/lib/tailwind'; import type { BlockProps } from '../Block'; -import { PlainCodeBlock } from '../CodeBlock'; -import { DocumentView } from '../DocumentView'; -import { Heading } from '../Heading'; import './scalar.css'; import './style.css'; import type { AnyOpenAPIOperationsBlock } from '@/lib/openapi/types'; +import { getOpenAPIContext } from './context'; /** * Render an openapi block or an openapi-operation block. @@ -55,56 +51,7 @@ async function OpenAPIOperationBody(props: BlockProps return ( , - chevronRight: , - plus: , - }, - renderCodeBlock: (codeProps) => , - renderDocument: (documentProps) => ( - - ), - renderHeading: (headingProps) => ( - div]:mt-0' - : undefined, - ])} - block={{ - object: 'block', - key: `${block.key}-heading`, - meta: block.meta, - data: {}, - type: 'heading-2', - nodes: [ - { - key: `${block.key}-heading-text`, - object: 'text', - leaves: [ - { text: headingProps.title, object: 'leaf', marks: [] }, - ], - }, - ], - }} - /> - ), - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - blockKey: block.key, - }} + context={getOpenAPIContext({ props, specUrl, context: context.contentContext })} className="openapi-block" /> ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx index b2a8415e1..06023a89c 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPISchemas.tsx @@ -1,6 +1,5 @@ import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock'; import { tcls } from '@/lib/tailwind'; -import { Icon } from '@gitbook/icons'; import { OpenAPISchemas as BaseOpenAPISchemas } from '@gitbook/react-openapi'; import type { BlockProps } from '../Block'; @@ -8,6 +7,7 @@ import type { BlockProps } from '../Block'; import './scalar.css'; import './style.css'; import type { OpenAPISchemasBlock } from '@/lib/openapi/types'; +import { getOpenAPIContext } from './context'; /** * Render an openapi-schemas block. @@ -49,19 +49,9 @@ async function OpenAPISchemasBody(props: BlockProps) { return ( , - chevronRight: , - plus: , - }, - defaultInteractiveOpened: context.mode === 'print', - id: block.meta?.id, - blockKey: block.key, - }} + context={getOpenAPIContext({ props, specUrl, context: context.contentContext })} className="openapi-block" /> ); diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx new file mode 100644 index 000000000..3453cf8d7 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPIWebhook.tsx @@ -0,0 +1,58 @@ +import { OpenAPIWebhook as BaseOpenAPIWebhook } from '@gitbook/react-openapi'; + +import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock'; +import { tcls } from '@/lib/tailwind'; + +import type { BlockProps } from '../Block'; + +import './scalar.css'; +import './style.css'; +import type { OpenAPIWebhookBlock } from '@/lib/openapi/types'; +import { getOpenAPIContext } from './context'; + +/** + * Render an openapi block or an openapi-webhook block. + */ +export async function OpenAPIWebhook(props: BlockProps) { + const { style } = props; + return ( +
+ +
+ ); +} + +async function OpenAPIWebhookBody(props: BlockProps) { + const { block, context } = props; + + if (!context.contentContext) { + return null; + } + + const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({ + block, + context: context.contentContext, + }); + + if (error) { + return ( +
+

+ Error with {specUrl}: {error.message} +

+
+ ); + } + + if (!data || !specUrl) { + return null; + } + + return ( + + ); +} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx new file mode 100644 index 000000000..8bd950757 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -0,0 +1,88 @@ +import type { JSONDocument } from '@gitbook/api'; +import { Icon } from '@gitbook/icons'; +import { type OpenAPIContextInput, checkIsValidLocale } from '@gitbook/react-openapi'; + +import { tcls } from '@/lib/tailwind'; + +import type { BlockProps } from '../Block'; +import { PlainCodeBlock } from '../CodeBlock'; +import { DocumentView } from '../DocumentView'; +import { Heading } from '../Heading'; + +import './scalar.css'; +import './style.css'; +import { DEFAULT_LOCALE, getCustomizationLocale } from '@/intl/server'; +import type { + AnyOpenAPIOperationsBlock, + OpenAPISchemasBlock, + OpenAPIWebhookBlock, +} from '@/lib/openapi/types'; +import type { GitBookAnyContext } from '@v2/lib/context'; + +/** + * Get the OpenAPI context to render a block. + */ +export function getOpenAPIContext(args: { + props: BlockProps; + specUrl: string; + context: GitBookAnyContext | undefined; +}): OpenAPIContextInput { + const { props, specUrl, context } = args; + const { block } = props; + + const customization = context && 'customization' in context ? context.customization : null; + const customizationLocale = customization + ? getCustomizationLocale(customization) + : DEFAULT_LOCALE; + const locale = checkIsValidLocale(customizationLocale) ? customizationLocale : DEFAULT_LOCALE; + + return { + specUrl, + icons: { + chevronDown: , + chevronRight: , + plus: , + }, + renderCodeBlock: (codeProps) => , + renderDocument: (documentProps) => ( + + ), + renderHeading: (headingProps) => ( + div]:mt-0' + : undefined, + ])} + block={{ + object: 'block', + key: `${block.key}-heading`, + meta: block.meta, + data: {}, + type: 'heading-2', + nodes: [ + { + key: `${block.key}-heading-text`, + object: 'text', + leaves: [{ text: headingProps.title, object: 'leaf', marks: [] }], + }, + ], + }} + /> + ), + defaultInteractiveOpened: props.context.mode === 'print', + id: block.meta?.id, + blockKey: block.key, + locale, + }; +} diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts b/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts index 25daa70d3..a0b24ee1a 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/index.ts @@ -1,2 +1,3 @@ export * from './OpenAPIOperation'; export * from './OpenAPISchemas'; +export * from './OpenAPIWebhook'; diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css index 3724dfb95..8096a9fbe 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/scalar.css @@ -1,5 +1,11 @@ @import "@scalar/api-client-react/style.css"; +html, +body { + /** Override Scalar's overscroll-behavior */ + @apply !overscroll-auto; +} + .light .scalar-modal-layout, .light .scalar-app, .light .scalar { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css index c0bf8d13c..213d12dec 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css @@ -1,5 +1,7 @@ /* Layout Components */ -.openapi-operation { +.openapi-operation, +.openapi-schemas, +.openapi-webhook { @apply flex-1 flex flex-col gap-8 mb-14 min-w-0; } @@ -7,6 +9,10 @@ @apply flex flex-col mb-14 flex-1; } +.openapi-schemas-title { + @apply tabular-nums text-[0.813rem] leading-4 font-mono shrink-0 font-medium text-tint-strong; +} + .openapi-columns { @apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch; } @@ -17,7 +23,7 @@ } .openapi-summary { - @apply flex flex-col items-start justify-start gap-3; + @apply flex flex-col items-start justify-start gap-3 scroll-m-12; } .openapi-summary-tags { @@ -29,10 +35,6 @@ @apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none; } -.openapi-stability-stable { - @apply text-green-600 dark:text-green-300 bg-green-50 dark:bg-green-900/6 ring-green-500/5; -} - .openapi-stability-alpha { @apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5; } @@ -49,10 +51,7 @@ @apply font-semibold font-mono truncate; } -.openapi-description.openapi-markdown { - @apply prose-sm text-[0.938rem]; -} - +.openapi-description.openapi-markdown, .openapi-description.openapi-markdown code { @apply prose-sm; } @@ -92,21 +91,23 @@ } /* Method Tags */ -.openapi-method { - @apply rounded uppercase font-mono font-bold text-xs px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap; +.openapi-method, +.openapi-statuscode { + @apply rounded uppercase font-mono items-center shrink-0 font-semibold text-[0.813rem] px-1 py-0.5 mr-2 text-tint-12/8 leading-tight align-middle inline-flex ring-1 ring-inset ring-tint-12/1 dark:ring-tint-1/1 whitespace-nowrap; } -.openapi-method-get { - /* @apply bg-[hsl(215,54%,86%)] dark:bg-[hsla(215,54%,45%,0.24)] dark:text-[hsl(215,54%,86%)]; */ +.openapi-method-get, +.openapi-statuscode-success { @apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100; } -.openapi-method-post { - /* @apply bg-[hsl(120,25%,80%)] dark:bg-[hsla(120,54%,32%,0.24)] dark:text-[hsl(120,25%,80%)]; */ +.openapi-method-post, +.openapi-statuscode-redirection { @apply bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-100; } -.openapi-method-put { +.openapi-method-put, +.openapi-statuscode-informational { @apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100; } @@ -114,8 +115,9 @@ @apply bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100; } -.openapi-method-delete { - @apply bg-pomegranate-100 text-pomegranate-800 dark:bg-pomegranate-900 dark:text-pomegranate-100; +.openapi-method-delete, +.openapi-statuscode-error { + @apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100; } .openapi-method-head, @@ -143,7 +145,7 @@ } .openapi-column-preview-body { - @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header-sections:page-has-toc:top-32 page-api-block:xl:max-2xl:top-32 print-mode:static; + @apply flex flex-col gap-4 sticky top-4 site-header:top-20 site-header:xl:max-2xl:top-32 site-header-sections:top-32 site-header-sections:xl:max-2xl:top-44 print-mode:static; } .openapi-column-preview pre { @@ -155,45 +157,30 @@ /* unstyled */ } -.openapi-schema-properties { - @apply flex flex-col; -} - -.openapi-schema { - @apply py-2.5 flex flex-col gap-2; +.openapi-schema-root-description.openapi-markdown { + @apply prose-sm text-balance mt-1.5 !text-[0.813rem] text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit; } -.openapi-section-body .openapi-schema-properties { - @apply divide-y divide-tint-subtle; +.openapi-section-schemas > .openapi-section-body > .openapi-schema-root-description { + @apply px-2.5 pt-2.5 mt-0 !text-sm; } -.openapi-disclosure-group-panel > .openapi-schema-properties > *:first-child > .openapi-schema { - @apply pt-0; -} - -.openapi-responsebody > .openapi-schema-properties > .openapi-schema:last-child { - @apply pb-0; +.openapi-schema-properties { + @apply flex flex-col; } -.openapi-responsebody > .openapi-schema-properties > .openapi-schema:only-child { - @apply py-0; +.openapi-schema, +.openapi-disclosure { + @apply py-2.5 flex flex-col gap-2; } .openapi-schema-properties .openapi-schema:last-child { @apply border-b-0; } -.openapi-schema-properties .openapi-schema-opened { - @apply pb-3; -} - -.openapi-schema > .openapi-schema-properties { - @apply mt-3; -} - /* Schema Presentation */ .openapi-schema-presentation { - @apply flex flex-col gap-1.5 font-normal; + @apply flex flex-col gap-1 font-normal; } .openapi-schema-properties:last-child { @@ -203,7 +190,7 @@ .openapi-schema-name { /* To make double click on the property name select only the name, we disable selection on the parent and re-enable it on the children. */ - @apply select-none flex gap-x-2.5 items-baseline text-sm flex-wrap; + @apply select-none text-sm text-balance *:whitespace-nowrap flex flex-wrap gap-y-1.5 gap-x-2.5; } .openapi-schema-name .openapi-deprecated { @@ -211,7 +198,7 @@ } .openapi-schema-propertyname { - @apply select-all font-mono font-normal text-tint-strong; + @apply select-all font-mono font-semibold text-tint-strong; } .openapi-schema-propertyname[data-deprecated="true"] { @@ -219,19 +206,23 @@ } .openapi-schema-required { - @apply text-warning-subtle text-[0.813rem]; + @apply text-warning-subtle text-[0.813rem] lowercase; } .openapi-schema-optional { - @apply text-info-subtle text-[0.813rem]; + @apply text-tint-subtle text-[0.813rem] lowercase; } .openapi-schema-readonly { - @apply text-primary-subtle/9 text-[0.813rem]; + @apply text-primary-subtle/9 text-[0.813rem] lowercase; } .openapi-schema-writeonly { - @apply text-success dark:text-success-subtle/9 text-[0.813rem]; + @apply text-success dark:text-success-subtle/9 text-[0.813rem] lowercase; +} + +.openapi-schema-types { + @apply flex items-baseline flex-wrap gap-1; } .openapi-schema-type { @@ -265,15 +256,11 @@ /* Schema Enum */ .openapi-schema-enum { - @apply flex flex-row text-sm leading-relaxed gap-2 flex-wrap text-tint; -} - -.openapi-schema-enum-list { - @apply flex flex-row gap-1.5 items-center; + @apply text-sm leading-relaxed max-w-full text-tint; } .openapi-schema-enum-value { - @apply text-sm; + @apply text-sm mr-1.5; } .openapi-schema-enum-value:first-child { @@ -286,7 +273,7 @@ /* Schema Description */ .openapi-schema-description.openapi-markdown { - @apply prose-sm text-tint overflow-hidden !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit; + @apply prose-sm text-tint overflow-hidden text-pretty !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit; } .openapi-schema-description.openapi-markdown pre:has(code) { @@ -306,14 +293,16 @@ /* Schema Examples */ .openapi-schema-example, -.openapi-schema-pattern { +.openapi-schema-pattern, +.openapi-schema-default { @apply prose-sm text-tint; } .openapi-schema-example code, .openapi-schema-pattern code, -.openapi-schema-enum-value code { - @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none; +.openapi-schema-enum-value code, +.openapi-schema-default code { + @apply py-px px-1 min-w-[1.625rem] text-tint-strong font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint-subtle bg-tint rounded text-xs leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none; } /* Authentication */ @@ -326,7 +315,7 @@ } .openapi-securities-description.openapi-markdown { - @apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit; + @apply prose-sm text-tint !font-normal select-text text-pretty prose-strong:font-semibold prose-strong:text-inherit; } .openapi-securities-label { @@ -352,12 +341,12 @@ } .openapi-requestbody-description.openapi-markdown { - @apply prose-sm text-tint !font-normal select-text prose-strong:font-semibold prose-strong:text-inherit; + @apply prose-sm text-tint !font-normal text-pretty select-text prose-strong:font-semibold prose-strong:text-inherit; } /* Responses */ .openapi-responses-header { - @apply py-2 border-b border-tint-subtle max-w-full flex-1; + @apply py-2 max-w-full flex-1; } .openapi-responses-header-content { @@ -365,31 +354,25 @@ } .openapi-response-tab-content { - @apply overflow-hidden max-w-full flex items-baseline gap-2; + @apply flex items-baseline truncate grow shrink max-w-max basis-[60%] mr-auto; + @apply text-left text-pretty relative leading-tight text-tint select-text; } .openapi-response-description.openapi-markdown { - @apply text-left prose-sm text-[0.813rem] h-auto relative leading-[1.125rem] text-tint !font-normal truncate select-text prose-strong:font-semibold prose-strong:text-inherit; + @apply text-left truncate prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit; } -.openapi-response-description.openapi-markdown::-webkit-scrollbar { - display: none; +.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-tab-content { + @apply basis-full; } -.openapi-response-description p { - @apply truncate max-w-full inline pr-1; -} - -.openapi-response-statuscode { - @apply tabular-nums text-sm font-normal font-mono shrink-0; -} - -.openapi-response-content-type { - @apply text-xs text-tint-8 ml-auto shrink-0; +.openapi-disclosure-group-trigger[aria-expanded="true"] + .openapi-response-description.openapi-markdown { + @apply whitespace-normal; } .openapi-response-body { - @apply flex flex-col gap-3; + @apply flex flex-col; } /* Response Body and Headers */ @@ -406,22 +389,39 @@ @apply px-3 py-1; } -.openapi-responsebody-header-content, -.openapi-responseheaders-header-content { - /* unstyled */ +/* Code Sample */ +.openapi-codesample-header { + @apply flex flex-row items-center; } -/* Code Sample */ -.openapi-codesample { - @apply border rounded bg-tint border-tint-subtle; +.openapi-response-media-types-examples-footer-content { + @apply flex flex-row items-center gap-2.5; } -.openapi-codesample-header { - @apply flex flex-row items-center; +.openapi-panel-heading, +.openapi-codesample-header, +.openapi-response-examples-header { + @apply border-b border-tint-subtle; +} + +.openapi-response-examples-header .openapi-select > button { + @apply max-w-full overflow-hidden shrink pl-0.5 py-0.5; +} + +.openapi-response-examples-header .openapi-select > button .openapi-statuscode { + @apply h-full; } .openapi-codesample-header-content { - @apply flex flex-row items-center h-fit; + @apply flex flex-row items-center justify-between h-fit p-2.5; +} + +.openapi-codesample-header-content .openapi-path { + @apply flex items-center font-mono text-[0.813rem] gap-1 h-fit *:truncate overflow-x-auto min-w-0 max-w-full font-normal text-tint-strong; +} + +.openapi-codesample-header-content .openapi-path .openapi-path-variable { + @apply text-[0.813rem]; } .openapi-codesample-footer { @@ -434,7 +434,7 @@ /* Path */ .openapi-path { - @apply flex items-start text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full; + @apply flex items-center text-sm gap-2 h-fit overflow-x-auto min-w-0 max-w-full; scrollbar-width: none; -ms-overflow-style: none; } @@ -448,12 +448,12 @@ } .openapi-path .openapi-method { - @apply text-[0.813rem] m-0 mt-0.5 items-center flex px-2; + @apply m-0 mt-0.5 items-center flex px-1; } .openapi-path-title { @apply flex-1 relative font-normal text-left font-mono text-tint-strong/10; - @apply py-0.5 px-1 rounded hover:bg-tint cursor-pointer transition-colors; + @apply py-0.5 px-1 rounded hover:bg-tint transition-colors; @apply whitespace-nowrap md:whitespace-normal; } @@ -465,14 +465,6 @@ display: none; } -/* .openapi-path-copy { - @apply absolute opacity-0 h-fit right-0 top-1/2 -translate-y-1/2 bg-light dark:bg-dark border rounded-md border-tint-subtle px-1.5 py-0; -} - -.openapi-path-title:hover .openapi-path-copy { - @apply opacity-11; -} */ - .openapi-path-title em { @apply not-italic text-primary font-medium; } @@ -485,18 +477,131 @@ @apply flex flex-row items-center py-2 px-3 justify-end border-t border-tint-subtle; } -/* Response Example */ -.openapi-response-example { - @apply border rounded bg-tint border-tint-subtle; +/* Panel */ +.openapi-panel, +.openapi-codesample, +.openapi-response-examples { + @apply border rounded-md straight-corners:rounded-none bg-tint-subtle border-tint-subtle shadow-sm; +} + +.openapi-panel pre, +.openapi-codesample pre, +.openapi-response-examples pre { + @apply bg-transparent border-none rounded-none shadow-none; +} + +.openapi-panel-heading { + @apply font-medium px-4 py-2 text-xs uppercase; +} + +.openapi-panel-body { + @apply relative; +} + +.openapi-panel-footer, +.openapi-codesample-footer { + @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint empty:hidden; +} + +.openapi-panel-footer .openapi-markdown { + @apply text-[0.813rem] text-tint; +} + +/* Example */ +.openapi-response-examples-header { + @apply flex flex-row items-center p-2.5; } -.openapi-response-example-empty { +.openapi-response-examples-header-content { + @apply max-w-full overflow-hidden truncate; +} + +.openapi-response-examples-statuscode-title { + @apply flex items-center; +} + +.openapi-response-examples-header .openapi-select > button, +.openapi-response-examples-header .openapi-markdown, +.openapi-response-examples-statuscode-title { + @apply text-[0.813rem] truncate text-tint font-normal; +} + +.openapi-response-examples-panel, +.openapi-codesample-panel { + @apply flex-1 text-sm relative focus-visible:outline-none; +} + +.openapi-example-empty { @apply relative text-tint bg-tint min-h-20 flex flex-col justify-center items-center; } /* Common Elements */ .openapi-select { - @apply max-w-60 rounded font-mono text-xs leading-6 px-1 py-0.5 truncate border border-tint-subtle bg-tint; + @apply w-auto max-w-full; +} + +/* Prevent react-aria popover from setting overflow:auto on body */ +body:has(.openapi-select-popover) { + overflow: unset !important; +} + +.openapi-select > button { + @apply flex items-center font-normal cursor-pointer *:truncate gap-1.5 text-tint-strong max-w-32 rounded text-xs p-1.5 leading-none border border-tint-subtle bg-tint; + @apply hover:bg-tint-hover transition-all; +} + +.openapi-select > button[data-focused="true"] { + @apply outline-primary -outline-offset-1 outline outline-1; +} + +.openapi-select > button > span.react-aria-SelectValue { + @apply shrink truncate flex items-center; +} + +.openapi-select > button > span.react-aria-SelectValue span:not(.openapi-statuscode) { + @apply truncate; +} + +.openapi-select > button .openapi-markdown { + @apply *:leading-none; +} + +.openapi-select > button .gb-icon { + @apply size-2.5 shrink-0; +} + +.openapi-select-popover { + @apply min-w-32 z-10 max-w-[max(20rem,var(--trigger-width))] overflow-x-hidden max-h-52 overflow-y-auto p-1.5 border border-tint-subtle bg-tint-base backdrop-blur-xl rounded-md straight-corners:rounded-none; + @apply shadow-md shadow-tint-12/1 dark:shadow-tint-1/1; +} + +.openapi-select-popover[data-entering] { + animation: popover-enter 0.2s ease-in-out; +} + +.openapi-select-popover[data-exiting] { + animation: popover-leave 0.2s ease-in-out; +} + +.openapi-select-item { + @apply text-sm flex items-center cursor-pointer px-1.5 overflow-hidden py-1 *:truncate text-tint ring-0 border-none rounded !outline-none; + @apply hover:bg-tint-hover theme-gradient:hover:bg-tint-12/1 hover:text-tint-strong contrast-more:hover:ring-1 contrast-more:hover:ring-inset contrast-more:hover:ring-current; +} + +.openapi-select button .openapi-markdown, +.openapi-select-item .openapi-markdown { + @apply text-[0.813rem] *:truncate; +} + +.openapi-select-item-selected, +.openapi-select-item-selected .openapi-markdown { + @apply text-primary-subtle hover:text-primary hover:bg-primary-hover; + @apply theme-muted:hover:bg-primary-active theme-gradient:hover:bg-primary-active tint:font-semibold; + @apply contrast-more:text-primary contrast-more:hover:text-primary-strong contrast-more:font-semibold; +} + +.openapi-select-listbox { + @apply flex flex-col gap-1 focus:ring-0 focus:outline-none; } .openapi-select:focus { @@ -524,6 +629,14 @@ @apply text-tint; } +.openapi-section-footer { + @apply flex flex-row items-center p-2.5 gap-2.5 text-sm text-tint-strong border-t border-tint-subtle; +} + +.openapi-section-footer-content { + @apply text-sm text-tint-strong; +} + .openapi-section-toggle { @apply text-tint-subtle contrast-more:text-tint-strong; } @@ -545,6 +658,7 @@ } /* Tabs */ +.openapi-panel-header, .openapi-tabs-list { @apply flex flex-row gap-1.5 py-1.5 px-2.5 w-full overflow-x-scroll; scrollbar-width: none; @@ -556,7 +670,7 @@ } .openapi-tabs-tab[aria-selected="true"] { - @apply text-primary after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all; + @apply !text-primary-subtle after:absolute after:-bottom-[calc(0.375rem_+_1px)] after:z-20 after:left-0 after:w-full after:h-px after:bg-primary-solid after:transition-all; } .openapi-tabs-panel { @@ -564,42 +678,33 @@ @apply before:w-full before:h-px before:absolute before:bg-tint-6 before:-top-px before:z-10; } -.openapi-tabs-footer { - @apply px-3 py-2 pt-2.5 border-t border-tint-subtle text-[0.813rem] text-tint; -} - -.openapi-tabs-footer .openapi-markdown { - @apply text-[0.813rem] text-tint; -} - /* Disclosure group */ .openapi-disclosure-group { - @apply border-b border-tint-subtle relative; + @apply border-tint-subtle transition-all border-b border-x overflow-auto last:rounded-b-md straight-corners:last:rounded-none first:rounded-t-md straight-corners:first:rounded-none first:border-t relative; } -.openapi-disclosure-group-header { - @apply flex flex-row items-baseline justify-between gap-3 relative; +.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover) { + @apply bg-tint-subtle; } -.openapi-disclosure-group-trigger { - @apply flex items-baseline relative flex-1 gap-2.5 py-2 truncate -outline-offset-1; +.openapi-disclosure-group:has(.openapi-disclosure-group-trigger:hover):has(.openapi-select:hover) { + @apply !bg-transparent; } -.openapi-disclosure-group-trigger:disabled { - @apply cursor-default; +.openapi-disclosure-group-trigger { + @apply flex w-full cursor-pointer items-baseline gap-3 transition-all relative flex-1 p-3 -outline-offset-1; } -.openapi-disclosure-group:only-child, -.openapi-disclosure-group:last-child { - @apply border-b-0; +.openapi-disclosure-group-label { + @apply flex flex-wrap items-baseline gap-x-3 gap-y-1 flex-1 truncate; } -.openapi-disclosure-group-trigger:disabled .openapi-disclosure-group-icon { - @apply invisible; +.openapi-disclosure-group-trigger[aria-disabled="true"] { + @apply cursor-default hover:bg-inherit; } -.openapi-disclosure-group-trigger[aria-expanded="true"] .openapi-response-description { - @apply whitespace-normal; +.openapi-disclosure-group-trigger[aria-disabled="true"] .openapi-disclosure-group-icon { + @apply invisible; } .openapi-disclosure-group-icon > svg { @@ -611,73 +716,120 @@ } .openapi-disclosure-group-panel { - @apply pb-2.5; + @apply px-3 transition-all; } .openapi-disclosure-group-trigger[aria-expanded="true"] > .openapi-disclosure-group-icon > svg { @apply rotate-90; } -.openapi-disclosure-group:hover .openapi-disclosure-group-mediatype { - @apply opacity-11 flex; +.openapi-disclosure-group-mediatype:not(:has(.openapi-select)) { + @apply text-[0.625rem] font-mono shrink-0 grow-0 text-tint-subtle contrast-more:text-tint; +} + +/* Disclosure */ +.openapi-schemas-disclosure > .openapi-disclosure-trigger { + @apply flex items-center font-mono transition-all text-tint-strong !text-sm hover:bg-tint-subtle relative flex-1 gap-2.5 p-3 truncate -outline-offset-1; } -.openapi-disclosure-group-mediatype { - @apply opacity-0 hidden text-xs transition-all duration-200 shrink-0 absolute right-0 top-2.5; +.openapi-schemas-disclosure > .openapi-disclosure-trigger, +.openapi-schemas-disclosure .openapi-disclosure-panel { + @apply straight-corners:!rounded-none; } -.openapi-disclosure-group-mediatype > span { - @apply px-1 bg-tint-6 text-tint-12 rounded-full; +.openapi-disclosure-panel { + @apply ml-1.5 pl-3 border-l border-tint-subtle; +} + +.openapi-schema .openapi-schema-properties .openapi-schema { + @apply animate-fadeIn [animation-fill-mode:both]; +} + +.openapi-schemas-disclosure > .openapi-disclosure-trigger[aria-expanded="true"] > svg { + @apply rotate-90; } -/* Disclosure */ .openapi-disclosure-trigger { - @apply transition-all truncate duration-300 max-w-full hover:text-tint-strong rounded-2xl border border-tint-subtle px-2.5 py-1 text-[0.813rem] text-tint flex flex-row items-center gap-1.5 -outline-offset-1; + @apply flex flex-row justify-between flex-wrap relative items-start gap-2 text-left -mx-3 px-3 -my-2.5 py-2.5 pr-10; } -.openapi-disclosure-trigger span { - @apply truncate; +.openapi-disclosure { + @apply -mx-3 px-3 py-2.5 transition-all flex flex-col ring-tint-subtle; } -.openapi-disclosure svg { - @apply size-3 shrink-0 transition-transform duration-300; +.openapi-disclosure:not( + .openapi-disclosure-group .openapi-disclosure, + .openapi-schema-alternatives .openapi-disclosure, + .openapi-schemas-disclosure .openapi-schema.openapi-disclosure + ) { + @apply rounded-xl; } -.openapi-disclosure-trigger[aria-expanded="true"] svg { - @apply rotate-45; +.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure { + @apply !rounded-none; } -.openapi-disclosure-trigger[aria-expanded="true"] { - @apply w-full rounded-lg border-b rounded-b-none; +.openapi-disclosure:has(> .openapi-disclosure-trigger:hover) { + @apply bg-tint-subtle overflow-hidden; } -.openapi-disclosure-trigger[aria-expanded="false"] { - @apply w-auto; +.openapi-disclosure:has(> .openapi-disclosure-trigger:hover), +.openapi-disclosure[data-expanded="true"] { + @apply ring-1 shadow-sm; } -.openapi-disclosure-panel[aria-hidden="false"] { - @apply border-b border-x border-tint-subtle rounded-b-lg; +.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) { + @apply mt-2; +} +.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) { + @apply mb-2; } -.openapi-disclosure-panel .openapi-schema { - @apply p-2.5; +.openapi-disclosure-trigger-label { + @apply absolute right-3 px-2 h-5 justify-end shrink-0 ring-tint-subtle truncate text-tint duration-300 transition-all rounded straight-corners:rounded-none flex flex-row gap-1 items-center text-xs; } -.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child, -.openapi-disclosure .openapi-schema-properties .openapi-schema:only-child .openapi-schema-name { - @apply !m-0; +.openapi-disclosure-trigger-label span { + @apply hidden; } -.openapi-disclosure .openapi-schema-properties .openapi-schema-enum { - @apply pt-0 mt-0; +.openapi-disclosure-trigger-label svg { + @apply size-3 shrink-0 transition-transform duration-300 text-tint-subtle; } -.openapi-section-body.openapi-schema.openapi-schema-root { - @apply space-y-2.5; +.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label, +.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label { + @apply shadow ring-1 bg-tint-base; +} + +.openapi-disclosure-trigger:hover > .openapi-disclosure-trigger-label span, +.openapi-disclosure-trigger[aria-expanded="true"] > .openapi-disclosure-trigger-label span { + @apply block animate-fadeIn; +} + +@media (hover: none) { + /* Make button label always visible on non-hover devices like phones */ + .openapi-disclosure-trigger-label { + @apply relative ring-1 bg-tint-base right-0; + } + .openapi-disclosure-trigger-label span { + @apply block; + } + .openapi-disclosure-trigger { + @apply pr-3; + } +} + +.openapi-disclosure-trigger[aria-expanded="true"] svg { + @apply rotate-45; } -.openapi-section-schemas { - @apply border border-tint-subtle rounded-lg; +.openapi-disclosure-trigger[aria-expanded="false"] { + @apply w-auto; +} + +.openapi-section-body.openapi-schema.openapi-schema-root { + @apply space-y-2.5; } .openapi-section-schemas > .openapi-section-body > .openapi-schema-properties > .openapi-schema, @@ -685,8 +837,18 @@ @apply p-2.5; } +.openapi-schema-alternatives { + @apply ml-1.5 pl-3 border-l border-tint-subtle; +} +.openapi-schema-alternative { + @apply relative; +} +.openapi-schema-alternative-separator { + @apply p-0.5 tracking-wide leading-none uppercase text-[0.625rem] text-tint-subtle whitespace-nowrap absolute -left-3 -bottom-2.5 -translate-x-1/2 z-10 bg-tint-base border-y border-tint-subtle -rotate-6; +} + .openapi-tooltip { - @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md font-medium px-1.5 py-0.5 shadow-sm text-[13px]; + @apply flex items-center gap-1 bg-tint-base border border-tint-subtle text-tint-strong rounded-md straight-corners:rounded-none font-medium px-1.5 py-0.5 shadow-sm text-[13px]; } .openapi-tooltip svg { @@ -719,6 +881,32 @@ } } +@keyframes popover-enter { + 0% { + opacity: 0; + transform: translateY(4px) scale(0.95); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes popover-leave { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(4px) scale(0.95); + } +} + .openapi-copy-button { - @apply hover:brightness-95; + @apply hover:brightness-95 cursor-pointer; +} + +.openapi-copy-button[data-disabled="true"] { + @apply cursor-default; } diff --git a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx index e95ffc754..ccb66babd 100644 --- a/packages/gitbook/src/components/DocumentView/ReusableContent.tsx +++ b/packages/gitbook/src/components/DocumentView/ReusableContent.tsx @@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api'; import { resolveContentRef } from '@/lib/references'; +import type { GitBookSpaceContext } from '@v2/lib/context'; import { getDataOrNull } from '@v2/lib/data'; import type { BlockProps } from './Block'; import { UnwrappedBlocks } from './Blocks'; @@ -13,15 +14,28 @@ export async function ReusableContent(props: BlockProps ); } diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx index 7c1ecb995..50dc0d827 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx @@ -115,7 +115,8 @@ export async function RecordColumnValue( return {''}; } - const alignment = getColumnAlignment(definition); + const horizontalAlignment = getColumnAlignment(definition); + const childrenHorizontalAlignment = `[&_*]:${horizontalAlignment}`; return ( ( 'lg:space-y-3', 'leading-normal', verticalAlignment, - alignment === 'right' ? 'text-right' : null, - alignment === 'center' ? 'text-center' : null, + horizontalAlignment, + childrenHorizontalAlignment, ]} context={context} blockStyle={['w-full', 'max-w-[unset]']} @@ -168,7 +169,7 @@ export async function RecordColumnValue( key={index} href={ref.href} target="_blank" - style={['flex', 'flex-row', 'items-center', 'gap-2']} + className="flex flex-row items-center gap-2" insights={ ref.file ? { diff --git a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx index e39f386cc..b09760ceb 100644 --- a/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/RecordRow.tsx @@ -15,14 +15,14 @@ export function RecordRow( fixedColumns: string[]; } ) { - const { view, autoSizedColumns, fixedColumns, block } = props; + const { view, autoSizedColumns, fixedColumns, block, context } = props; return (
{view.columns.map((column) => { const columnWidth = getColumnWidth({ column, - columnWidths: view.columnWidths, + columnWidths: context.mode === 'print' ? undefined : view.columnWidths, autoSizedColumns, fixedColumns, }); diff --git a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx index 65bae8e0b..9f2b98ba4 100644 --- a/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx +++ b/packages/gitbook/src/components/DocumentView/Table/ViewGrid.tsx @@ -13,10 +13,10 @@ import { getColumnAlignment } from './utils'; 3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px */ export function ViewGrid(props: TableViewProps) { - const { block, view, records, style } = props; + const { block, view, records, style, context } = props; /* Calculate how many columns are auto-sized vs fixed width */ - const columnWidths = view.columnWidths; + const columnWidths = context.mode === 'print' ? undefined : view.columnWidths; const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]); const fixedColumns = view.columns.filter((column) => columnWidths?.[column]); @@ -42,32 +42,28 @@ export function ViewGrid(props: TableViewProps) { )} >
- {view.columns.map((column) => { - const alignment = getColumnAlignment(block.data.definition[column]); - return ( -
- {block.data.definition[column].title} -
- ); - })} + {view.columns.map((column) => ( +
+ {block.data.definition[column].title} +
+ ))}
)} diff --git a/packages/gitbook/src/components/DocumentView/Table/table.module.css b/packages/gitbook/src/components/DocumentView/Table/table.module.css index 4b602a607..53065b9d6 100644 --- a/packages/gitbook/src/components/DocumentView/Table/table.module.css +++ b/packages/gitbook/src/components/DocumentView/Table/table.module.css @@ -23,7 +23,7 @@ } .columnHeader { - @apply text-sm font-medium py-2 px-4 text-tint-strong; + @apply text-sm font-medium py-2 px-3 text-tint-strong; } .row { diff --git a/packages/gitbook/src/components/DocumentView/Table/utils.ts b/packages/gitbook/src/components/DocumentView/Table/utils.ts index 01bd82bcd..1fc95b357 100644 --- a/packages/gitbook/src/components/DocumentView/Table/utils.ts +++ b/packages/gitbook/src/components/DocumentView/Table/utils.ts @@ -1,4 +1,5 @@ import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api'; +import assertNever from 'assert-never'; /** * Get the value for a column in a record. @@ -14,11 +15,24 @@ export function getRecordValue @@ -165,16 +168,14 @@ export function DynamicTabs( )} > {tabs.map((tab) => ( - + + + +
))}
{tabs.map((tab, index) => ( diff --git a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx index 4d5a0d555..d1ab24485 100644 --- a/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx +++ b/packages/gitbook/src/components/DocumentView/Tabs/Tabs.tsx @@ -39,6 +39,7 @@ export function Tabs(props: BlockProps) { ) { ); } - return ; + return ( + + ); } diff --git a/packages/gitbook/src/components/DocumentView/Text.tsx b/packages/gitbook/src/components/DocumentView/Text.tsx index 0117a05d6..87efd8863 100644 --- a/packages/gitbook/src/components/DocumentView/Text.tsx +++ b/packages/gitbook/src/components/DocumentView/Text.tsx @@ -5,6 +5,8 @@ import type { DocumentMarkItalic, DocumentMarkKeyboard, DocumentMarkStrikethrough, + DocumentMarkSubscript, + DocumentMarkSuperscript, DocumentText, DocumentTextMark, } from '@gitbook/api'; @@ -47,6 +49,8 @@ const MARK_STYLES = { strikethrough: Strikethrough, color: Color, keyboard: Keyboard, + superscript: Superscript, + subscript: Subscript, }; interface MarkedLeafProps { @@ -74,6 +78,14 @@ function Keyboard(props: MarkedLeafProps) { ); } +function Superscript(props: MarkedLeafProps) { + return {props.children}; +} + +function Subscript(props: MarkedLeafProps) { + return {props.children}; +} + function Code(props: MarkedLeafProps) { return ( 0; + const hasCopyright = customization.footer.copyright; + const hasThemeToggle = customization.themes.toggeable; + + const mobileOnly = !hasLogo && !hasGroups && !hasCopyright && hasThemeToggle; + return (
-
-
+
- {/* Footer Logo */} - {customization.footer.logo ? ( - Logo - ) : null} + { + // Footer Logo + customization.footer.logo ? ( +
+ Logo +
+ ) : null + } - {/* Mode Switcher */} - {customization.themes.toggeable ? ( -
- - - -
- ) : null} + { + // Theme Toggle + customization.themes.toggeable ? ( +
+ + + +
+ ) : null + } - {/* Navigation Groups (split into equal columns) */} - {customization.footer.groups?.length > 0 ? ( -
- {partition(customization.footer.groups, FOOTER_COLUMNS).map( - (column, columnIndex) => ( -
- {column.map((group, groupIndex) => ( - - ))} -
- ) - )} -
- ) : null} + { + // Navigation groups (split into equal columns) + customization.footer.groups?.length > 0 ? ( +
+
+ {partition(customization.footer.groups, FOOTER_COLUMNS).map( + (column, columnIndex) => ( +
+ {column.map((group, groupIndex) => ( + + ))} +
+ ) + )} +
+
+ ) : null + } - {/* Legal */} -
- {customization.footer.copyright ? ( -

{customization.footer.copyright}

- ) : null} -
+ { + // Legal + customization.footer.copyright ? ( +
+

{customization.footer.copyright}

+
+ ) : null + }
-
diff --git a/packages/gitbook/src/components/Header/Dropdown.tsx b/packages/gitbook/src/components/Header/Dropdown.tsx deleted file mode 100644 index 2d078c3b9..000000000 --- a/packages/gitbook/src/components/Header/Dropdown.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { Icon } from '@gitbook/icons'; -import { type DetailedHTMLProps, type HTMLAttributes, useId } from 'react'; - -import { type ClassValue, tcls } from '@/lib/tailwind'; - -import { Link, type LinkInsightsProps } from '../primitives'; - -export type DropdownButtonProps = Omit< - Partial, E>>, - 'ref' ->; - -/** - * Button with a dropdown. - */ -export function Dropdown(props: { - /** Content of the button */ - button: (buttonProps: DropdownButtonProps) => React.ReactNode; - /** Content of the dropdown */ - children: React.ReactNode; - /** Custom styles */ - className?: ClassValue; -}) { - const { button, children, className } = props; - const dropdownId = useId(); - - return ( -
- {button({ - id: dropdownId, - tabIndex: 0, - 'aria-expanded': true, - 'aria-haspopup': true, - })} -
-
-
- {children} -
-
-
-
- ); -} - -/** - * Animated chevron to display in the dropdown button. - */ -export function DropdownChevron() { - return ( - - ); -} - -/** - * Group of menu items in a dropdown. - */ -export function DropdownMenu(props: { children: React.ReactNode }) { - const { children } = props; - - return
{children}
; -} - -/** - * Menu item in a dropdown. - */ -export function DropdownMenuItem( - props: { - href: string | null; - active?: boolean; - className?: ClassValue; - children: React.ReactNode; - } & LinkInsightsProps -) { - const { children, active = false, href, className, insights } = props; - - if (href) { - return ( - - {children} - - ); - } - - return ( -
{children}
- ); -} diff --git a/packages/gitbook/src/components/Header/DropdownMenu.tsx b/packages/gitbook/src/components/Header/DropdownMenu.tsx new file mode 100644 index 000000000..8f51e4685 --- /dev/null +++ b/packages/gitbook/src/components/Header/DropdownMenu.tsx @@ -0,0 +1,176 @@ +'use client'; + +import { Icon } from '@gitbook/icons'; +import type { DetailedHTMLProps, HTMLAttributes } from 'react'; +import { useState } from 'react'; + +import { type ClassValue, tcls } from '@/lib/tailwind'; + +import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; + +import { Link, type LinkInsightsProps } from '../primitives'; + +export type DropdownButtonProps = Omit< + Partial, E>>, + 'ref' +>; + +/** + * Button with a dropdown. + */ +export function DropdownMenu(props: { + /** Content of the button */ + button: React.ReactNode; + /** Content of the dropdown */ + children: React.ReactNode; + /** Custom styles */ + className?: ClassValue; + /** Open the dropdown on hover */ + openOnHover?: boolean; +}) { + const { button, children, className, openOnHover = false } = props; + const [hovered, setHovered] = useState(false); + const [clicked, setClicked] = useState(false); + + return ( + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => (openOnHover ? setClicked(!clicked) : null)} + className="group/dropdown" + > + {button} + + + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + align="start" + className="z-40 animate-present pt-2" + > +
+ {children} +
+
+
+
+ ); +} + +/** + * Animated chevron to display in the dropdown button. + */ +export function DropdownChevron() { + return ( + + ); +} + +/** + * Button with a chevron for use in dropdowns. + */ +export function DropdownButton(props: { + children: React.ReactNode; + className?: ClassValue; +}) { + const { children, className } = props; + + return ( +
+ {children} + +
+ ); +} + +/** + * Menu item in a dropdown. + */ +export function DropdownMenuItem( + props: { + href: string | null; + active?: boolean; + className?: ClassValue; + children: React.ReactNode; + } & LinkInsightsProps +) { + const { children, active = false, href, className, insights } = props; + + const itemClassName = tcls( + 'rounded straight-corners:rounded-sm px-3 py-1 text-sm', + active + ? 'bg-primary text-primary-strong data-[highlighted]:bg-primary-hover' + : 'data-[highlighted]:bg-tint-hover', + 'focus:outline-none', + className + ); + + if (href) { + return ( + + + {children} + + + ); + } + + return ( + + {children} + + ); +} + +export function DropdownSubMenu(props: { children: React.ReactNode; label: React.ReactNode }) { + const { children, label } = props; + + return ( + + + {label} + + + + +
+ {children} +
+
+
+
+ ); +} diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 7fa3f4888..35c2583c4 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -215,7 +215,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo />
)} - {sections && ( + {sections && sections.list.length > 1 && ( diff --git a/packages/gitbook/src/components/Header/HeaderLink.tsx b/packages/gitbook/src/components/Header/HeaderLink.tsx index 1a66e13b3..fcfb28c72 100644 --- a/packages/gitbook/src/components/Header/HeaderLink.tsx +++ b/packages/gitbook/src/components/Header/HeaderLink.tsx @@ -13,12 +13,11 @@ import { tcls } from '@/lib/tailwind'; import { Button, Link } from '../primitives'; import { - Dropdown, type DropdownButtonProps, DropdownChevron, DropdownMenu, DropdownMenuItem, -} from './Dropdown'; +} from './DropdownMenu'; export async function HeaderLink(props: { context: GitBookSiteContext; @@ -33,21 +32,13 @@ export async function HeaderLink(props: { if (link.links && link.links.length > 0) { return ( - { - if (!target || !link.to) { - return ( - - ); - } - return ( + button={ + !target || !link.to ? ( + + ) : ( - ); - }} + ) + } + openOnHover={true} > - - {link.links.map((subLink, index) => ( - - ))} - - + {link.links.map((subLink, index) => ( + + ))} + ); } @@ -142,10 +132,9 @@ function HeaderItemButton( position: SiteInsightsLinkPosition.Header, }, }} + label={title} {...rest} - > - {title} - + /> ); } @@ -158,10 +147,12 @@ function getHeaderLinkClassName(_props: { headerPreset: CustomizationHeaderPrese 'text-tint', 'links-default:hover:text-primary', + 'links-default:data-[state=open]:text-primary', 'links-default:tint:hover:text-tint-strong', - + 'links-default:tint:data-[state=open]:text-tint-strong', 'underline-offset-2', 'links-accent:hover:underline', + 'links-accent:data-[state=open]:underline', 'links-accent:underline-offset-4', 'links-accent:decoration-primary-subtle', 'links-accent:decoration-[3px]', diff --git a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx index 5d27fc9a8..7954b80ec 100644 --- a/packages/gitbook/src/components/Header/HeaderLinkMore.tsx +++ b/packages/gitbook/src/components/Header/HeaderLinkMore.tsx @@ -10,7 +10,7 @@ import type React from 'react'; import { resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; -import { Dropdown, DropdownChevron, DropdownMenu, DropdownMenuItem } from './Dropdown'; +import { DropdownChevron, DropdownMenu, DropdownMenuItem, DropdownSubMenu } from './DropdownMenu'; import styles from './headerLinks.module.css'; /** @@ -23,7 +23,7 @@ export function HeaderLinkMore(props: { }) { const { label, links, context } = props; - const renderButton = () => ( + const renderButton = (
); } @@ -70,32 +69,28 @@ async function MoreMenuLink(props: { const target = link.to ? await resolveContentRef(link.to, context) : null; - return ( - <> - {'links' in link && link.links.length > 0 && ( -
- )} - - {link.title} - - {'links' in link - ? link.links.map((subLink, index) => ( - - )) - : null} - + return 'links' in link && link.links.length > 0 ? ( + + {link.links.map((subLink, index) => { + return ; + })} + + ) : ( + + {link.title} + ); } diff --git a/packages/gitbook/src/components/Header/HeaderLogo.tsx b/packages/gitbook/src/components/Header/HeaderLogo.tsx index 08a1d2d10..72d0b4798 100644 --- a/packages/gitbook/src/components/Header/HeaderLogo.tsx +++ b/packages/gitbook/src/components/Header/HeaderLogo.tsx @@ -20,7 +20,7 @@ export async function HeaderLogo(props: HeaderLogoProps) { return ( {customization.header.logo ? ( @@ -48,8 +48,6 @@ export async function HeaderLogo(props: HeaderLogoProps) { ]} priority="high" style={tcls( - 'rounded', - 'straight-corners:rounded-sm', 'overflow-hidden', 'shrink', 'min-w-0', diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index 39e81e59f..9728216b7 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -3,7 +3,7 @@ import type { SiteSpace } from '@gitbook/api'; import { tcls } from '@/lib/tailwind'; import type { GitBookSiteContext } from '@v2/lib/context'; -import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown'; +import { DropdownChevron, DropdownMenu } from './DropdownMenu'; import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem'; export function SpacesDropdown(props: { @@ -16,14 +16,13 @@ export function SpacesDropdown(props: { const { linker } = context; return ( - ( + button={
{siteSpace.title}
- )} + } > - - {siteSpaces.map((otherSiteSpace, index) => ( - - ))} - -
+ {siteSpaces.map((otherSiteSpace, index) => ( + + ))} + ); } diff --git a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx index 8c3676ff8..70859abfa 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdownMenuItem.tsx @@ -4,7 +4,7 @@ import type { Space } from '@gitbook/api'; import { joinPath } from '@/lib/paths'; import { useCurrentPagePath } from '../hooks'; -import { DropdownMenuItem } from './Dropdown'; +import { DropdownMenuItem } from './DropdownMenu'; function useVariantSpaceHref(variantSpaceUrl: string) { const currentPathname = useCurrentPagePath(); diff --git a/packages/gitbook/src/components/Insights/InsightsProvider.tsx b/packages/gitbook/src/components/Insights/InsightsProvider.tsx index 002d7e00c..7ea3fe5a3 100644 --- a/packages/gitbook/src/components/Insights/InsightsProvider.tsx +++ b/packages/gitbook/src/components/Insights/InsightsProvider.tsx @@ -8,6 +8,7 @@ import { useDebounceCallback, useEventCallback } from 'usehooks-ts'; import type { VisitorAuthClaims } from '@/lib/adaptive'; import { getAllBrowserCookiesMap } from '@/lib/browser-cookies'; import { getSession } from './sessions'; +import { useVisitedPages } from './useVisitedPages'; import { getVisitorId } from './visitorId'; export type InsightsEventName = api.SiteInsightsEvent['type']; @@ -63,9 +64,19 @@ type TrackEventCallback = ( const InsightsContext = React.createContext(() => {}); interface InsightsProviderProps extends InsightsEventContext { + /** If true, the events will be sent to the server. */ enabled: boolean; + + /** If true, the visitor cookie tracking will be used */ + visitorCookieTrackingEnabled: boolean; + + /** The URL of the app. */ appURL: string; + + /** The host of the API. */ apiHost: string; + + /** The children of the provider. */ children: React.ReactNode; } @@ -73,8 +84,9 @@ interface InsightsProviderProps extends InsightsEventContext { * Wrap the content of the app with the InsightsProvider to track events. */ export function InsightsProvider(props: InsightsProviderProps) { - const { enabled, appURL, apiHost, children, ...context } = props; + const { enabled, appURL, apiHost, children, visitorCookieTrackingEnabled, ...context } = props; + const addVisitedPage = useVisitedPages((state) => state.addPage); const visitorIdRef = React.useRef(null); const eventsRef = React.useRef<{ [pathname: string]: @@ -124,6 +136,14 @@ export function InsightsProvider(props: InsightsProviderProps) { ...eventsForPathname, events: [], }; + + // Mark the page as visited in our local state + if (eventsForPathname.pageContext.pageId) { + addVisitedPage({ + spaceId: context.spaceId, + pageId: eventsForPathname.pageContext.pageId, + }); + } } if (allEvents.length > 0) { @@ -140,7 +160,8 @@ export function InsightsProvider(props: InsightsProviderProps) { }); const flushBatchedEvents = useDebounceCallback(async () => { - const visitorId = visitorIdRef.current ?? (await getVisitorId(appURL)); + const visitorId = + visitorIdRef.current ?? (await getVisitorId(appURL, visitorCookieTrackingEnabled)); visitorIdRef.current = visitorId; flushEventsSync(); @@ -184,7 +205,7 @@ export function InsightsProvider(props: InsightsProviderProps) { * Get the visitor ID and store it in a ref. */ React.useEffect(() => { - getVisitorId(appURL).then((visitorId) => { + getVisitorId(appURL, visitorCookieTrackingEnabled).then((visitorId) => { visitorIdRef.current = visitorId; // When the page is unloaded, flush all events, but only if the visitor ID is set window.addEventListener('beforeunload', flushEventsSync); @@ -192,7 +213,7 @@ export function InsightsProvider(props: InsightsProviderProps) { return () => { window.removeEventListener('beforeunload', flushEventsSync); }; - }, [flushEventsSync, appURL]); + }, [flushEventsSync, appURL, visitorCookieTrackingEnabled]); return ( diff --git a/packages/gitbook/src/components/Insights/index.ts b/packages/gitbook/src/components/Insights/index.ts index ff922086e..792440a5c 100644 --- a/packages/gitbook/src/components/Insights/index.ts +++ b/packages/gitbook/src/components/Insights/index.ts @@ -2,3 +2,4 @@ export * from './InsightsProvider'; export * from './visitorId'; export * from './cookies'; export * from './TrackPageViewEvent'; +export * from './useVisitedPages'; diff --git a/packages/gitbook/src/components/Insights/useVisitedPages.tsx b/packages/gitbook/src/components/Insights/useVisitedPages.tsx new file mode 100644 index 000000000..f5450b432 --- /dev/null +++ b/packages/gitbook/src/components/Insights/useVisitedPages.tsx @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +type VisitedPage = { + spaceId: string; + pageId: string; +}; + +/** + * A store for the pages that have been visited in the current session. + */ +export const useVisitedPages = create<{ + pages: VisitedPage[]; + addPage: (page: VisitedPage) => void; +}>((set) => ({ + pages: [], + addPage: (page) => + set((state) => { + const lastPage = state.pages[state.pages.length - 1]; + if (lastPage && lastPage.spaceId === page.spaceId && lastPage.pageId === page.pageId) { + return { pages: state.pages }; + } + + return { pages: [...state.pages, page] }; + }), +})); diff --git a/packages/gitbook/src/components/Insights/visitorId.ts b/packages/gitbook/src/components/Insights/visitorId.ts index 8c2b7d8dd..aa7d6733f 100644 --- a/packages/gitbook/src/components/Insights/visitorId.ts +++ b/packages/gitbook/src/components/Insights/visitorId.ts @@ -13,10 +13,13 @@ let pendingVisitorId: Promise | null = null; /** * Return the current visitor identifier. */ -export async function getVisitorId(appURL: string): Promise { +export async function getVisitorId( + appURL: string, + visitorCookieTrackingEnabled: boolean +): Promise { if (!visitorId) { if (!pendingVisitorId) { - pendingVisitorId = fetchVisitorID(appURL).finally(() => { + pendingVisitorId = fetchVisitorID(appURL, visitorCookieTrackingEnabled).finally(() => { pendingVisitorId = null; }); } @@ -30,10 +33,13 @@ export async function getVisitorId(appURL: string): Promise { /** * Propose a visitor identifier to the GitBook.com server and get the devideId back. */ -async function fetchVisitorID(appURL: string): Promise { +async function fetchVisitorID( + appURL: string, + visitorCookieTrackingEnabled: boolean +): Promise { const withoutCookies = isCookiesTrackingDisabled(); - if (withoutCookies) { + if (withoutCookies || !visitorCookieTrackingEnabled) { return generateRandomId(); } diff --git a/packages/gitbook/src/components/PDF/PDFPage.tsx b/packages/gitbook/src/components/PDF/PDFPage.tsx index 081fadc02..96cb784cc 100644 --- a/packages/gitbook/src/components/PDF/PDFPage.tsx +++ b/packages/gitbook/src/components/PDF/PDFPage.tsx @@ -20,7 +20,6 @@ import { TrademarkLink } from '@/components/TableOfContents/Trademark'; import type { PolymorphicComponentProp } from '@/components/utils/types'; import { getSpaceLanguage } from '@/intl/server'; import { tString } from '@/intl/translate'; -import { getPagePDFContainerId } from '@/lib/links'; import { resolvePageId } from '@/lib/pages'; import { tcls } from '@/lib/tailwind'; import { defaultCustomization } from '@/lib/utils'; @@ -29,6 +28,7 @@ import { type PDFSearchParams, getPDFSearchParams } from './urls'; import { PageControlButtons } from './PageControlButtons'; import { PrintButton } from './PrintButton'; import './pdf.css'; +import { sanitizeGitBookAppURL } from '@/lib/app'; const DEFAULT_LIMIT = 100; @@ -92,7 +92,10 @@ export async function PDFPage(props: { diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx index a51410cbc..06821d964 100644 --- a/packages/gitbook/src/components/PageAside/PageAside.tsx +++ b/packages/gitbook/src/components/PageAside/PageAside.tsx @@ -87,7 +87,7 @@ export function PageAside(props: { 'page-api-block:xl:max-2xl:dark:hover:shadow-tint-1/1', 'page-api-block:xl:max-2xl:rounded-md', 'page-api-block:xl:max-2xl:h-auto', - 'page-api-block:xl:max-2xl:my-8', + 'page-api-block:xl:max-2xl:my-4', 'page-api-block:p-2' )} > diff --git a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx index a2adf315e..d9b02ebdf 100644 --- a/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx +++ b/packages/gitbook/src/components/PageAside/ScrollSectionsList.tsx @@ -123,7 +123,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) { > {section.tag ? ( {section.tag} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 54623ad66..6d548dec9 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -7,7 +7,6 @@ import { t } from '@/intl/translate'; import { hasFullWidthBlock, isNodeEmpty } from '@/lib/document'; import type { AncestorRevisionPage } from '@/lib/pages'; import { tcls } from '@/lib/tailwind'; - import { DocumentView, DocumentViewSkeleton } from '../DocumentView'; import { TrackPageViewEvent } from '../Insights'; import { PageFeedbackForm } from '../PageFeedback'; diff --git a/packages/gitbook/src/components/PageBody/PageCover.tsx b/packages/gitbook/src/components/PageBody/PageCover.tsx index 2239512f4..d434d5eba 100644 --- a/packages/gitbook/src/components/PageBody/PageCover.tsx +++ b/packages/gitbook/src/components/PageBody/PageCover.tsx @@ -1,12 +1,14 @@ import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api'; import type { GitBookSiteContext } from '@v2/lib/context'; +import type { StaticImageData } from 'next/image'; import { Image, type ImageSize } from '@/components/utils'; import { resolveContentRef } from '@/lib/references'; import { tcls } from '@/lib/tailwind'; -import defaultPageCover from './default-page-cover.svg'; +import defaultPageCoverSVG from './default-page-cover.svg'; +const defaultPageCover = defaultPageCoverSVG as StaticImageData; const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 }; /** diff --git a/packages/gitbook/src/components/PageBody/PageHeader.tsx b/packages/gitbook/src/components/PageBody/PageHeader.tsx index 42564c84e..b8b62f945 100644 --- a/packages/gitbook/src/components/PageBody/PageHeader.tsx +++ b/packages/gitbook/src/components/PageBody/PageHeader.tsx @@ -27,7 +27,7 @@ export async function PageHeader(props: { > {ancestors.length > 0 && (
); diff --git a/packages/gitbook/src/fonts/custom.test.ts b/packages/gitbook/src/fonts/custom.test.ts index ce2616187..b430b7624 100644 --- a/packages/gitbook/src/fonts/custom.test.ts +++ b/packages/gitbook/src/fonts/custom.test.ts @@ -28,6 +28,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, multiWeight: { @@ -81,6 +84,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, multiSource: { @@ -99,6 +105,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, missingFormat: { @@ -117,6 +126,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, empty: { @@ -124,6 +136,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { custom: true, fontFamily: 'Empty Font', fontFaces: [], + permissions: { + edit: false, + }, }, specialChars: { @@ -136,6 +151,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }], }, ], + permissions: { + edit: false, + }, }, complex: { @@ -158,6 +176,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, variousURLs: { @@ -174,6 +195,9 @@ const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = { ], }, ], + permissions: { + edit: false, + }, }, }; diff --git a/packages/gitbook/src/fonts/custom.ts b/packages/gitbook/src/fonts/custom.ts index cb31f771e..84abdf8b2 100644 --- a/packages/gitbook/src/fonts/custom.ts +++ b/packages/gitbook/src/fonts/custom.ts @@ -1,9 +1,9 @@ -import type { CustomizationFontDefinition } from '@gitbook/api'; +import type { CustomizationFontDefinitionInput } from '@gitbook/api'; /** * Define the custom font faces and set the --font-custom to the custom font name */ -export function generateFontFacesCSS(customFont: CustomizationFontDefinition): string { +export function generateFontFacesCSS(customFont: CustomizationFontDefinitionInput): string { const { fontFaces } = customFont; // Generate font face declarations for all weights @@ -45,7 +45,7 @@ export function generateFontFacesCSS(customFont: CustomizationFontDefinition): s /** * Get a list of font sources to preload (only 400 and 700 weights) */ -export function getFontSourcesToPreload(customFont: CustomizationFontDefinition) { +export function getFontSourcesToPreload(customFont: CustomizationFontDefinitionInput) { return customFont.fontFaces.filter( (face): face is typeof face & { weight: 400 | 700 } => face.weight === 400 || face.weight === 700 diff --git a/packages/gitbook/src/intl/server.ts b/packages/gitbook/src/intl/server.ts index 0aa201ef2..b13076f41 100644 --- a/packages/gitbook/src/intl/server.ts +++ b/packages/gitbook/src/intl/server.ts @@ -4,17 +4,26 @@ import { type TranslationLanguage, languages } from './translations'; export * from './translate'; +export const DEFAULT_LOCALE = 'en'; + +/** + * Get the locale of the customization. + */ +export function getCustomizationLocale(customization: SiteCustomizationSettings): string { + return customization.internationalization.locale; +} + /** * Create the translation context for a space to use in the server components. */ export function getSpaceLanguage(customization: SiteCustomizationSettings): TranslationLanguage { - const fallback = languages.en; + const fallback = languages[DEFAULT_LOCALE]; - const { locale } = customization.internationalization; + const locale = getCustomizationLocale(customization); let language = fallback; // @ts-ignore - if (locale !== 'en' && languages[locale]) { + if (locale !== DEFAULT_LOCALE && languages[locale]) { // @ts-ignore language = languages[locale]; } diff --git a/packages/gitbook/src/intl/translations/de.ts b/packages/gitbook/src/intl/translations/de.ts index 72f2563a6..b42da6c55 100644 --- a/packages/gitbook/src/intl/translations/de.ts +++ b/packages/gitbook/src/intl/translations/de.ts @@ -58,4 +58,9 @@ export const de = { 'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.', pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.', more: 'Mehr', + link_tooltip_external_link: 'Externe Verlinkung zu', + link_tooltip_page_anchor: 'Zum Abschnitt springen', + link_tooltip_ai_summary: 'Seitenhighlight', + link_tooltip_ai_summary_description: 'Basierend auf Ihrem Kontext. Kann Fehler enthalten.', + open_in_new_tab: 'In neuem Tab öffnen', }; diff --git a/packages/gitbook/src/intl/translations/en.ts b/packages/gitbook/src/intl/translations/en.ts index c6e976784..df9cc5099 100644 --- a/packages/gitbook/src/intl/translations/en.ts +++ b/packages/gitbook/src/intl/translations/en.ts @@ -36,7 +36,7 @@ export const en = { table_of_contents_button_label: 'Open table of contents', cookies_title: 'Cookies', cookies_prompt: - 'This site uses cookies to deliver its service and to analyse traffic. By browsing this site, you accept the ${1}.', + 'This site uses cookies to deliver its service and to analyze traffic. By browsing this site, you accept the ${1}.', cookies_prompt_privacy: 'privacy policy', cookies_accept: 'Accept', cookies_reject: 'Reject', @@ -56,4 +56,9 @@ export const en = { pdf_limit_reached: "Couldn't generate the PDF for ${1} pages, generation stopped at ${2}.", pdf_limit_reached_continue: 'Extend with ${1} more pages.', more: 'More', + link_tooltip_external_link: 'External link to', + link_tooltip_page_anchor: 'Jump to section', + link_tooltip_ai_summary: 'Page highlight', + link_tooltip_ai_summary_description: 'Based on your context. May contain mistakes.', + open_in_new_tab: 'Open in new tab', }; diff --git a/packages/gitbook/src/intl/translations/es.ts b/packages/gitbook/src/intl/translations/es.ts index 11fb4dbb0..2d0d46565 100644 --- a/packages/gitbook/src/intl/translations/es.ts +++ b/packages/gitbook/src/intl/translations/es.ts @@ -60,4 +60,9 @@ export const es: TranslationLanguage = { 'No se pudo generar el PDF para ${1} páginas, la generación se detuvo en ${2}.', pdf_limit_reached_continue: 'Extender con ${1} páginas más.', more: 'Más', + link_tooltip_external_link: 'Enlace externo a', + link_tooltip_page_anchor: 'Saltar a la sección', + link_tooltip_ai_summary: 'Resumen de la página', + link_tooltip_ai_summary_description: 'Basado en tu contexto. Puede contener errores.', + open_in_new_tab: 'Abrir en una nueva pestaña', }; diff --git a/packages/gitbook/src/intl/translations/fr.ts b/packages/gitbook/src/intl/translations/fr.ts index c5edac9ad..71d340332 100644 --- a/packages/gitbook/src/intl/translations/fr.ts +++ b/packages/gitbook/src/intl/translations/fr.ts @@ -58,4 +58,9 @@ export const fr: TranslationLanguage = { pdf_limit_reached: 'Impossible de générer le PDF pour ${1} pages, génération arrêtée à ${2}.', pdf_limit_reached_continue: 'Étendre avec ${1} pages supplémentaires.', more: 'Plus', + link_tooltip_external_link: 'Lien externe à', + link_tooltip_page_anchor: 'Sauter à la section', + link_tooltip_ai_summary: 'Résumé de la page', + link_tooltip_ai_summary_description: 'Basé sur votre contexte. Peut contenir des erreurs.', + open_in_new_tab: 'Ouvrir dans un nouvel onglet', }; diff --git a/packages/gitbook/src/intl/translations/ja.ts b/packages/gitbook/src/intl/translations/ja.ts index 08a11ae23..f3f548068 100644 --- a/packages/gitbook/src/intl/translations/ja.ts +++ b/packages/gitbook/src/intl/translations/ja.ts @@ -58,4 +58,10 @@ export const ja: TranslationLanguage = { pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。', pdf_limit_reached_continue: 'さらに${1}ページで拡張', more: '詳細', + link_tooltip_external_link: '外部リンク先', + link_tooltip_page_anchor: 'ページ内リンク先', + link_tooltip_ai_summary: 'ページのハイライト', + link_tooltip_ai_summary_description: + 'あなたのコンテキストに基づいています。間違いが含まれる可能性があります。', + open_in_new_tab: '新しいタブで開く', }; diff --git a/packages/gitbook/src/intl/translations/nl.ts b/packages/gitbook/src/intl/translations/nl.ts index 2304b5fd9..6bfb45e9a 100644 --- a/packages/gitbook/src/intl/translations/nl.ts +++ b/packages/gitbook/src/intl/translations/nl.ts @@ -58,4 +58,9 @@ export const nl: TranslationLanguage = { pdf_limit_reached: "Kon de PDF niet genereren voor ${1} pagina's, generatie gestopt bij ${2}.", pdf_limit_reached_continue: 'Verleng met ${1} extra pagina’s.', more: 'Meer', + link_tooltip_external_link: 'Externe link naar', + link_tooltip_page_anchor: 'Spring naar sectie', + link_tooltip_ai_summary: 'Pagina-samenvatting', + link_tooltip_ai_summary_description: 'Gebaseerd op je context. Kan fouten bevatten.', + open_in_new_tab: 'Open in nieuw tabblad', }; diff --git a/packages/gitbook/src/intl/translations/no.ts b/packages/gitbook/src/intl/translations/no.ts index 21d08ca4f..6be6b413e 100644 --- a/packages/gitbook/src/intl/translations/no.ts +++ b/packages/gitbook/src/intl/translations/no.ts @@ -58,4 +58,9 @@ export const no: TranslationLanguage = { pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, generering stoppet ved ${2}.', pdf_limit_reached_continue: 'Utvid med ${1} flere sider.', more: 'Mer', + link_tooltip_external_link: 'Ekstern lenke til', + link_tooltip_page_anchor: 'Hopp til seksjon', + link_tooltip_ai_summary: 'Sidesammendrag', + link_tooltip_ai_summary_description: 'Basert på din kontekst. Kan inneholde feil.', + open_in_new_tab: 'Åpne i ny fane', }; diff --git a/packages/gitbook/src/intl/translations/pt-br.ts b/packages/gitbook/src/intl/translations/pt-br.ts index f49f3f805..35a40cd45 100644 --- a/packages/gitbook/src/intl/translations/pt-br.ts +++ b/packages/gitbook/src/intl/translations/pt-br.ts @@ -58,4 +58,9 @@ export const pt_br = { 'Não foi possível gerar o PDF para ${1} páginas, generation stopped at ${2}.', pdf_limit_reached_continue: 'Extender com mais ${1} páginas.', more: 'Mais', + link_tooltip_external_link: 'Link externo para', + link_tooltip_page_anchor: 'Pular para a seção', + link_tooltip_ai_summary: 'Resumo da página', + link_tooltip_ai_summary_description: 'Baseado no seu contexto. Pode conter erros.', + open_in_new_tab: 'Abrir em uma nova guia', }; diff --git a/packages/gitbook/src/intl/translations/translations.test.ts b/packages/gitbook/src/intl/translations/translations.test.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/gitbook/src/intl/translations/zh.ts b/packages/gitbook/src/intl/translations/zh.ts index 34a08fde3..08efdddde 100644 --- a/packages/gitbook/src/intl/translations/zh.ts +++ b/packages/gitbook/src/intl/translations/zh.ts @@ -56,4 +56,9 @@ export const zh: TranslationLanguage = { pdf_limit_reached: '无法为${1}页生成 PDF,生成在${2}页时停止。', pdf_limit_reached_continue: '使用${1}页进行扩展。', more: '更多', + link_tooltip_external_link: '外部链接到', + link_tooltip_page_anchor: '跳转到页面', + link_tooltip_ai_summary: '页面要点', + link_tooltip_ai_summary_description: '基于您的上下文。可能包含错误。', + open_in_new_tab: '在新标签页中打开', }; diff --git a/packages/gitbook/src/lib/api.ts b/packages/gitbook/src/lib/api.ts index 6694a8dd2..310074450 100644 --- a/packages/gitbook/src/lib/api.ts +++ b/packages/gitbook/src/lib/api.ts @@ -242,7 +242,7 @@ export const getLatestOpenAPISpecVersionContent = cache({ * Resolve a URL to the content to render. */ export const getPublishedContentByUrl = cache({ - name: 'api.getPublishedContentByUrl.v4', + name: 'api.getPublishedContentByUrl.v7', tag: (url) => getCacheTagForURL(url), get: async ( url: string, @@ -267,6 +267,11 @@ export const getPublishedContentByUrl = cache({ const parsed = parseCacheResponse(response); + // biome-ignore lint/suspicious/noConsole: log the ttl of the token + console.log( + `Parsed ttl: ${parsed.ttl} at ${Date.now()}, for ${'apiToken' in response.data ? response.data.apiToken : ''}` + ); + const data: PublishedContentWithCache = { ...response.data, cacheMaxAge: parsed.ttl, @@ -760,7 +765,12 @@ export const getComputedDocument = cache({ * Mimic the validation done on source server-side to reduce API usage. */ function validateSiteRedirectSource(source: string) { - return source.length <= 512 && /^\/[a-zA-Z0-9-_.\\/]+[a-zA-Z0-9-_.]$/.test(source); + return ( + source.length <= 512 && + /^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/.test( + source + ) + ); } /** diff --git a/packages/gitbook/src/lib/app.ts b/packages/gitbook/src/lib/app.ts new file mode 100644 index 000000000..5233861fe --- /dev/null +++ b/packages/gitbook/src/lib/app.ts @@ -0,0 +1,27 @@ +import { GITBOOK_APP_URL } from '@v2/lib/env'; + +/** + * Create an absolute href in the GitBook application. + */ +export function getGitBookAppHref(pathname: string): string { + const appUrl = new URL(GITBOOK_APP_URL); + appUrl.pathname = pathname; + + return appUrl.toString(); +} + +/** + * Sanitize a URL to be a valid GitBook.com app URL. + */ +export function sanitizeGitBookAppURL(input: string): string | null { + if (!URL.canParse(input)) { + return null; + } + + const url = new URL(input); + if (url.origin !== GITBOOK_APP_URL) { + return null; + } + + return url.toString(); +} diff --git a/packages/gitbook/src/lib/assets.ts b/packages/gitbook/src/lib/assets.ts index b073422ca..94ec5efe7 100644 --- a/packages/gitbook/src/lib/assets.ts +++ b/packages/gitbook/src/lib/assets.ts @@ -1,8 +1,12 @@ -import { GITBOOK_ASSETS_URL } from '@v2/lib/env'; +import { GITBOOK_ASSETS_URL, GITBOOK_URL } from '@v2/lib/env'; +import { joinPath, joinPathWithBaseURL } from './paths'; /** * Create a public URL for an asset. */ export function getAssetURL(path: string): string { - return `${GITBOOK_ASSETS_URL || ''}/~gitbook/static/${path}`; + return joinPathWithBaseURL( + GITBOOK_ASSETS_URL || GITBOOK_URL, + joinPath('~gitbook/static', path) + ); } diff --git a/packages/gitbook/src/lib/cache/cloudflare-do.ts b/packages/gitbook/src/lib/cache/cloudflare-do.ts index 11ebc9274..c02883235 100644 --- a/packages/gitbook/src/lib/cache/cloudflare-do.ts +++ b/packages/gitbook/src/lib/cache/cloudflare-do.ts @@ -64,7 +64,10 @@ export const cloudflareDOCache: CacheBackend = { return; } - const keys = await stub.purge(); + const keys = await retryOnDurableObjectError(async () => { + return await stub.purge(); + }); + keys.forEach((key) => { entries.push({ key, tag }); }); @@ -99,3 +102,50 @@ async function getStub(tag: string): Promise { return stub; } + +/** + * Retry an operation on a Durable Object if it fails with a retriable error. + * It will retry up to 4 times with an exponential backoff. + */ +export async function retryOnDurableObjectError( + operation: () => T | Promise, + attemptsLeft = 4, + delay = 50 +): Promise { + if (attemptsLeft <= 0) { + return operation(); + } + + try { + return await operation(); + } catch (error) { + if (!shouldRetryError(error)) { + throw error; + } + + if (attemptsLeft > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)); + return retryOnDurableObjectError(operation, attemptsLeft - 1, delay * 2); + } + + throw error; + } +} + +const RETRIABLE_ERROR_MESSAGES = new Set([ + 'Cannot resolve Durable Object due to transient issue on remote node.', + 'internal error', + `Durable Object's isolate exceeded its memory limit and was reset.`, + 'cannot access storage because object has moved to a different machine', + 'Durable Object reset because its code was updated.', + "The Durable Object's code has been updated, this version can no longer access storage.", + // https://developers.cloudflare.com/workers/observability/errors/#runtime-errors + 'Network connection lost.', +]); + +/** + * Check if an error should be retried based on its error message. + */ +function shouldRetryError(error: unknown): boolean { + return error instanceof Error && RETRIABLE_ERROR_MESSAGES.has(error.message); +} diff --git a/packages/gitbook/src/lib/cache/revalidateTags.ts b/packages/gitbook/src/lib/cache/revalidateTags.ts index 5bff70e48..b05313208 100644 --- a/packages/gitbook/src/lib/cache/revalidateTags.ts +++ b/packages/gitbook/src/lib/cache/revalidateTags.ts @@ -36,18 +36,25 @@ export async function revalidateTags(tags: string[]): Promise<{ await Promise.all( cacheBackends.map(async (backend, backendIndex) => { - const { entries: addedEntries } = await backend.revalidateTags(tags); + try { + const { entries: addedEntries } = await backend.revalidateTags(tags); - addedEntries.forEach(({ key, tag }) => { - stats[key] = stats[key] ?? { - tag, - backends: {}, - }; - stats[key].backends[backend.name] = { set: true }; + addedEntries.forEach(({ key, tag }) => { + stats[key] = stats[key] ?? { + tag, + backends: {}, + }; + stats[key].backends[backend.name] = { set: true }; - entries.set(key, { tag, key }); - keysByBackend.set(backendIndex, [...(keysByBackend.get(backendIndex) ?? []), key]); - }); + entries.set(key, { tag, key }); + keysByBackend.set(backendIndex, [ + ...(keysByBackend.get(backendIndex) ?? []), + key, + ]); + }); + } catch (err) { + throw new Error(`error revalidating tags on backend ${backend.name}: ${err}`); + } }) ); @@ -71,7 +78,8 @@ export async function revalidateTags(tags: string[]): Promise<{ return; } } - throw error; + + throw new Error(`error deleting entries on backend ${backend.name}: ${error}`); } } }) diff --git a/packages/gitbook/src/lib/document-sections.ts b/packages/gitbook/src/lib/document-sections.ts index 3f8dbb13a..38619a588 100644 --- a/packages/gitbook/src/lib/document-sections.ts +++ b/packages/gitbook/src/lib/document-sections.ts @@ -3,6 +3,7 @@ import type { GitBookAnyContext } from '@v2/lib/context'; import { getNodeText } from './document'; import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock'; +import { resolveOpenAPISchemasBlock } from './openapi/resolveOpenAPISchemasBlock'; export interface DocumentSection { id: string; @@ -52,6 +53,26 @@ export async function getDocumentSections( }); } } + + if ( + block.type === 'openapi-schemas' && + !block.data.grouped && + block.meta?.id && + block.data.schemas.length === 1 + ) { + const { data } = await resolveOpenAPISchemasBlock({ + block, + context, + }); + const schema = data?.schemas[0]; + if (schema) { + sections.push({ + id: block.meta.id, + title: `The ${schema.name} object`, + depth: 1, + }); + } + } } return sections; diff --git a/packages/gitbook/src/lib/links.ts b/packages/gitbook/src/lib/links.ts deleted file mode 100644 index 056e48b20..000000000 --- a/packages/gitbook/src/lib/links.ts +++ /dev/null @@ -1,153 +0,0 @@ -import 'server-only'; - -import { - type RevisionPage, - type RevisionPageDocument, - type RevisionPageGroup, - RevisionPageType, -} from '@gitbook/api'; -import { headers } from 'next/headers'; - -import { GITBOOK_APP_URL } from '@v2/lib/env'; -import { getPagePath } from './pages'; -import { withLeadingSlash, withTrailingSlash } from './paths'; -import { assertIsNotV2 } from './v2'; - -export interface PageHrefContext { - /** - * If defined, we are generating a PDF of the specific page IDs, - * and these pages will be rendered in the same HTML output. - */ - pdf?: string[]; -} - -/** - * Return the base path for the current request. - * The value will start and finish with / - */ -export async function getBasePath(): Promise { - assertIsNotV2(); - const headersList = await headers(); - const path = headersList.get('x-gitbook-basepath') ?? '/'; - - return withTrailingSlash(withLeadingSlash(path)); -} - -/** - * Return the site base path for the current request. - * The value will start and finish with / - */ -export async function getSiteBasePath(): Promise { - assertIsNotV2(); - const headersList = await headers(); - const path = headersList.get('x-gitbook-site-basepath') ?? '/'; - - return withTrailingSlash(withLeadingSlash(path)); -} - -/** - * Return the current host for the current request. - */ -export async function getHost(): Promise { - assertIsNotV2(); - const headersList = await headers(); - const mode = headersList.get('x-gitbook-mode'); - if (mode === 'proxy') { - return headersList.get('x-forwarded-host') ?? ''; - } - - return headersList.get('x-gitbook-host') ?? headersList.get('host') ?? ''; -} - -/** - * Return the root URL for the GitBook Open instance (not the content). - * Use `baseUrl` to get the base URL for the current content. - * - * The URL will end with "/". - */ -export async function getRootUrl(): Promise { - assertIsNotV2(); - const [headersList, host] = await Promise.all([headers(), getHost()]); - const protocol = headersList.get('x-forwarded-proto') ?? 'https'; - let path = headersList.get('x-gitbook-origin-basepath') ?? '/'; - - if (!path.startsWith('/')) { - path = `/${path}`; - } - - if (!path.endsWith('/')) { - path = `${path}/`; - } - - return `${protocol}://${host}${path}`; -} - -/** - * Return the base URL for the current content. - * The URL will end with "/". - */ -export async function getBaseUrl(): Promise { - assertIsNotV2(); - const [headersList, host, basePath] = await Promise.all([headers(), getHost(), getBasePath()]); - const protocol = headersList.get('x-forwarded-proto') ?? 'https'; - return `${protocol}://${host}${basePath}`; -} - -/** - * Create an absolute href in the current content. - */ -export async function getAbsoluteHref(href: string, withHost = false): Promise { - assertIsNotV2(); - const base = withHost ? await getBaseUrl() : await getBasePath(); - return `${base}${href.startsWith('/') ? href.slice(1) : href}`; -} - -/** - * Create an absolute href in the GitBook application. - */ -export function getGitbookAppHref(pathname: string): string { - const appUrl = new URL(GITBOOK_APP_URL); - appUrl.pathname = pathname; - - return appUrl.toString(); -} - -/** - * Create a link to a page path in the current space. - */ -export async function getPageHref( - rootPages: RevisionPage[], - page: RevisionPageDocument | RevisionPageGroup, - context: PageHrefContext = {}, - /** Anchor to link to in the page. */ - anchor?: string -): Promise { - assertIsNotV2(); - const { pdf } = context; - - if (pdf) { - if (pdf.includes(page.id)) { - return `#${getPagePDFContainerId(page, anchor)}`; - } - if (page.type === RevisionPageType.Group) { - return '#'; - } - - // Use an absolute URL to the page - return page.urls.app; - } - - const href = - (await getAbsoluteHref(getPagePath(rootPages, page))) + (anchor ? `#${anchor}` : ''); - return href; -} - -/** - * Create the HTML ID for the container of a page during a PDF rendering. - */ -export function getPagePDFContainerId( - page: RevisionPageDocument | RevisionPageGroup, - anchor?: string -): string { - return `pdf-page-${page.id}${anchor ? `-${anchor}` : ''}`; -} diff --git a/packages/gitbook/src/lib/openapi/fetch.ts b/packages/gitbook/src/lib/openapi/fetch.ts index 93151fe43..b496bbe59 100644 --- a/packages/gitbook/src/lib/openapi/fetch.ts +++ b/packages/gitbook/src/lib/openapi/fetch.ts @@ -4,16 +4,18 @@ import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cac import type { AnyOpenAPIOperationsBlock, OpenAPISchemasBlock, + OpenAPIWebhookBlock, ResolveOpenAPIBlockArgs, } from '@/lib/openapi/types'; -import { memoize } from '@v2/lib/data/memoize'; +import { getCloudflareRequestGlobal } from '@v2/lib/data/cloudflare'; +import { withCacheKey, withoutConcurrentExecution } from '@v2/lib/data/memoize'; import { assert } from 'ts-essentials'; import { resolveContentRef } from '../references'; import { isV2 } from '../v2'; import { enrichFilesystem } from './enrich'; import type { FetchOpenAPIFilesystemResult } from './types'; -type AnyOpenAPIBlock = AnyOpenAPIOperationsBlock | OpenAPISchemasBlock; +type AnyOpenAPIBlock = AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock; /** * Fetch OpenAPI block. @@ -66,15 +68,16 @@ const fetchFilesystemV1 = cache({ }, }); -const fetchFilesystemV2 = memoize(async function fetchFilesystemV2(url: string) { - 'use cache'; - - // TODO: add cache lifetime once we can use next.js 15 code here - - const response = await fetchFilesystemUncached(url); +const fetchFilesystemV2 = withCacheKey( + withoutConcurrentExecution(getCloudflareRequestGlobal, async (_cacheKey, url: string) => { + return fetchFilesystemUseCache(url); + }) +); - return response; -}); +const fetchFilesystemUseCache = async (url: string) => { + 'use cache'; + return fetchFilesystemUncached(url); +}; async function fetchFilesystemUncached( url: string, diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts index 5c6b64b3b..82c10d60b 100644 --- a/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPISchemasBlock.ts @@ -1,5 +1,5 @@ -import { OpenAPIParseError } from '@gitbook/openapi-parser'; -import { type OpenAPISchemasData, resolveOpenAPISchemas } from '@gitbook/react-openapi'; +import { OpenAPIParseError, type OpenAPISchema } from '@gitbook/openapi-parser'; +import { resolveOpenAPISchemas } from '@gitbook/react-openapi'; import { fetchOpenAPIFilesystem } from './fetch'; import type { OpenAPISchemasBlock, @@ -7,7 +7,9 @@ import type { ResolveOpenAPIBlockResult, } from './types'; -type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult; +type ResolveOpenAPISchemasBlockResult = ResolveOpenAPIBlockResult<{ + schemas: OpenAPISchema[]; +}>; const weakmap = new WeakMap>(); diff --git a/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts new file mode 100644 index 000000000..817e080c3 --- /dev/null +++ b/packages/gitbook/src/lib/openapi/resolveOpenAPIWebhookBlock.ts @@ -0,0 +1,61 @@ +import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch'; +import { OpenAPIParseError } from '@gitbook/openapi-parser'; +import { type OpenAPIWebhookData, resolveOpenAPIWebhook } from '@gitbook/react-openapi'; +import type { + OpenAPIWebhookBlock, + ResolveOpenAPIBlockArgs, + ResolveOpenAPIBlockResult, +} from './types'; + +type ResolveOpenAPIWebhookBlockResult = ResolveOpenAPIBlockResult; + +const weakmap = new WeakMap>(); + +/** + * Cache the result of resolving an OpenAPI block. + * It is important because the resolve is called in sections and in the block itself. + */ +export function resolveOpenAPIWebhookBlock( + args: ResolveOpenAPIBlockArgs +): Promise { + if (weakmap.has(args.block)) { + return weakmap.get(args.block)!; + } + + const result = baseResolveOpenAPIWebhookBlock(args); + weakmap.set(args.block, result); + return result; +} + +/** + * Resolve OpenAPI webhook block. + */ +async function baseResolveOpenAPIWebhookBlock( + args: ResolveOpenAPIBlockArgs +): Promise { + const { context, block } = args; + if (!block.data.name || !block.data.method) { + return { data: null, specUrl: null }; + } + + try { + const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); + + if (!filesystem) { + return { data: null, specUrl: null }; + } + + const data = await resolveOpenAPIWebhook(filesystem, { + name: block.data.name, + method: block.data.method, + }); + + return { data, specUrl }; + } catch (error) { + if (error instanceof OpenAPIParseError) { + return { error }; + } + + throw error; + } +} diff --git a/packages/gitbook/src/lib/openapi/types.ts b/packages/gitbook/src/lib/openapi/types.ts index 99810d25b..af6997c3f 100644 --- a/packages/gitbook/src/lib/openapi/types.ts +++ b/packages/gitbook/src/lib/openapi/types.ts @@ -2,6 +2,7 @@ import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation, DocumentBlockOpenAPISchemas, + DocumentBlockOpenAPIWebhook, } from '@gitbook/api'; import type { Filesystem, OpenAPIParseError, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import type { GitBookAnyContext } from '@v2/lib/context'; @@ -16,6 +17,11 @@ export type AnyOpenAPIOperationsBlock = DocumentBlockOpenAPI | DocumentBlockOpen */ export type OpenAPISchemasBlock = DocumentBlockOpenAPISchemas; +/** + * Type for OpenAPI Webhook block + */ +export type OpenAPIWebhookBlock = DocumentBlockOpenAPIWebhook; + /** * Arguments for resolving OpenAPI block. */ diff --git a/packages/gitbook/src/lib/paths.test.ts b/packages/gitbook/src/lib/paths.test.ts new file mode 100644 index 000000000..1d418f498 --- /dev/null +++ b/packages/gitbook/src/lib/paths.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'bun:test'; +import { getExtension } from './paths'; + +describe('getExtension', () => { + it('should return the extension of a path', () => { + expect(getExtension('test.txt')).toBe('.txt'); + }); + + it('should return an empty string if there is no extension', () => { + expect(getExtension('test/path/to/file')).toBe(''); + }); + + it('should return the extension of a path with multiple dots', () => { + expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt'); + }); +}); diff --git a/packages/gitbook/src/lib/paths.ts b/packages/gitbook/src/lib/paths.ts index eff69a569..ebdf35cd3 100644 --- a/packages/gitbook/src/lib/paths.ts +++ b/packages/gitbook/src/lib/paths.ts @@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string { return pathname; } + +/** + * Get the extension of a path. + */ +export function getExtension(path: string): string { + const re = /\.[0-9a-z]+$/i; + const match = path.match(re); + if (match) { + return match[0]; + } + return ''; +} diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 83b5a6f09..7248ab2de 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -1,6 +1,7 @@ import type { ContentRef, RevisionFile, + RevisionPageDocument, RevisionReusableContent, SiteSpace, Space, @@ -14,11 +15,12 @@ import type React from 'react'; import { PageIcon } from '@/components/PageIcon'; +import { getGitBookAppHref } from './app'; import { getBlockById, getBlockTitle } from './document'; -import { getGitbookAppHref } from './links'; import { resolvePageId } from './pages'; import { findSiteSpaceById } from './sites'; import type { ClassValue } from './tailwind'; +import { filterOutNullable } from './typescript'; export interface ResolvedContentRef { /** Text to render in the content ref */ @@ -29,14 +31,22 @@ export interface ResolvedContentRef { icon?: React.ReactNode; /** Emoji associated with the reference */ emoji?: string; + /** The content ref's ancestors */ + ancestors?: { icon?: React.ReactNode; label: string; href?: string }[]; /** URL to open for the content ref */ href: string; /** True if the content ref is active */ active: boolean; /** File, if the reference is a file */ file?: RevisionFile; - /** Resolved reusable content, if the ref points to reusable content on a revision. */ - reusableContent?: RevisionReusableContent; + /** Page document resolved from the content ref */ + page?: RevisionPageDocument; + /** Resolved reusable content, if the ref points to reusable content on a revision. Also contains the space and revision used for resolution. */ + reusableContent?: { + revisionReusableContent: RevisionReusableContent; + space: Space; + revision: string; + }; /** Resolve OpenAPI spec filesystem. */ openAPIFilesystem?: Filesystem; } @@ -115,6 +125,14 @@ export async function resolveContentRef( : resolvePageId(pages, contentRef.page); const page = resolvePageResult?.page; + const ancestors = + resolvePageResult?.ancestors.map((ancestor) => ({ + label: ancestor.title, + icon: , + href: resolveAsAbsoluteURL + ? linker.toAbsoluteURL(linker.toPathForPage({ page: ancestor, pages })) + : linker.toPathForPage({ page: ancestor, pages }), + })) ?? []; if (!page) { return null; } @@ -125,10 +143,16 @@ export async function resolveContentRef( let text = ''; let icon: React.ReactNode | undefined = undefined; let emoji: string | undefined = undefined; + const href = linker.toPathForPage({ page, pages, anchor }); // Compute the text to display for the link if (anchor) { text = `#${anchor}`; + ancestors.push({ + label: page.title, + icon: , + href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href, + }); if (resolveAnchorText) { const document = await getPageDocument(dataFetcher, space, page); @@ -151,13 +175,14 @@ export async function resolveContentRef( icon = ; } - const href = linker.toPathForPage({ page, pages, anchor }); - return { href: resolveAsAbsoluteURL ? linker.toAbsoluteURL(href) : href, text, + subText: page.description, + ancestors: ancestors, emoji, icon, + page, active: !anchor && page.id === activePage?.id, }; } @@ -173,7 +198,7 @@ export async function resolveContentRef( if (!targetSpace) { return { - href: getGitbookAppHref(`/s/${contentRef.space}`), + href: getGitBookAppHref(`/s/${contentRef.space}`), text: 'space', active: false, }; @@ -203,28 +228,59 @@ export async function resolveContentRef( case 'collection': { return { - href: getGitbookAppHref('/home'), + href: getGitBookAppHref('/home'), text: 'collection', active: false, }; } case 'reusable-content': { + // Figure out which space and revision the reusable content is in. + const container: { space: Space; revision: string } | null = await (async () => { + // without a space on the content ref, or if the space is the same as the current one, we can use the current revision. + if (!contentRef.space || contentRef.space === context.space.id) { + return { space: context.space, revision: revisionId }; + } + + const space = await getDataOrNull( + dataFetcher.getSpace({ + spaceId: contentRef.space, + shareKey: undefined, + }) + ); + + if (!space) { + return null; + } + + return { space, revision: space.revision }; + })(); + + if (!container) { + return null; + } + const reusableContent = await getDataOrNull( dataFetcher.getReusableContent({ - spaceId: space.id, - revisionId, + spaceId: container.space.id, + revisionId: container.revision, reusableContentId: contentRef.reusableContent, }) ); + if (!reusableContent) { return null; } + return { - href: getGitbookAppHref(`/s/${space.id}`), + href: getGitBookAppHref(`/s/${container.space}/~/reusable/${reusableContent.id}`), text: reusableContent.title, active: false, - reusableContent, + reusableContent: { + revisionReusableContent: reusableContent, + space: container.space, + revision: container.revision, + }, }; } @@ -346,6 +402,12 @@ async function resolveContentRefInSpace( return { ...resolved, - subText: space.title, + ancestors: [ + { + label: space.title, + href: baseURL.toString(), + }, + ...(resolved.ancestors ?? []), + ].filter(filterOutNullable), }; } diff --git a/packages/gitbook/src/lib/tracing.ts b/packages/gitbook/src/lib/tracing.ts index 38db33924..71b5c072e 100644 --- a/packages/gitbook/src/lib/tracing.ts +++ b/packages/gitbook/src/lib/tracing.ts @@ -28,19 +28,19 @@ export async function trace( }; const start = now(); - let failed = false; + let traceError: null | Error = null; try { return await fn(span); } catch (error) { span.setAttribute('error', true); - failed = true; + traceError = error as Error; throw error; } finally { if (process.env.SILENT !== 'true' && process.env.NODE_ENV !== 'development') { const end = now(); // biome-ignore lint/suspicious/noConsole: we want to log performance data console.log( - `trace ${completeName} ${failed ? 'failed' : 'succeeded'} in ${end - start}ms`, + `trace ${completeName} ${traceError ? `failed with ${traceError.message}` : 'succeeded'} in ${end - start}ms`, attributes ); } diff --git a/packages/gitbook/src/lib/urls.ts b/packages/gitbook/src/lib/urls.ts new file mode 100644 index 000000000..0bfa89e76 --- /dev/null +++ b/packages/gitbook/src/lib/urls.ts @@ -0,0 +1,10 @@ +/** + * Check if a URL is an HTTP URL. + */ +export function checkIsHttpURL(input: string | URL): boolean { + if (!URL.canParse(input)) { + return false; + } + const parsed = new URL(input); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; +} diff --git a/packages/gitbook/src/lib/utils.ts b/packages/gitbook/src/lib/utils.ts index 7421ccb80..0db784cf2 100644 --- a/packages/gitbook/src/lib/utils.ts +++ b/packages/gitbook/src/lib/utils.ts @@ -26,6 +26,9 @@ export function defaultCustomization(): api.SiteCustomizationSettings { internationalization: { locale: api.CustomizationLocale.En, }, + insights: { + trackingCookie: true, + }, favicon: {}, header: { preset: api.CustomizationHeaderPreset.Default, diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index 7c4c723e6..eb2accdc2 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -8,7 +8,9 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types'; import { createImageResizer } from '@v2/lib/images'; import { createLinker } from '@v2/lib/links'; +import { GitBookAPI } from '@gitbook/api'; import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data'; +import { headers } from 'next/headers'; import { type SiteContentPointer, type SpaceContentPointer, @@ -29,9 +31,11 @@ import { getUserById, renderIntegrationUi, searchSiteContent, + withAPI as withAPIV1, } from './api'; import { getDynamicCustomizationSettings } from './customization'; -import { getBasePath, getHost, getSiteBasePath } from './links'; +import { withLeadingSlash, withTrailingSlash } from './paths'; +import { assertIsNotV2 } from './v2'; /* * Code that will be used until the migration to v2 is complete. @@ -56,7 +60,7 @@ export async function getV1BaseContext(): Promise { return url; }; - const dataFetcher = await getDataFetcherV1(); + const dataFetcher = getDataFetcherV1(); const imageResizer = createImageResizer({ imagesContextId: host, @@ -80,77 +84,121 @@ export async function getV1BaseContext(): Promise { * Try not to use this as much as possible, and instead take the data fetcher from the props. * This data fetcher should only be used at the top of the tree. */ -async function getDataFetcherV1(): Promise { +function getDataFetcherV1(apiTokenOverride?: string): GitBookDataFetcher { + let apiClient: GitBookAPI | undefined; + + /** + * Run a function with the correct API client. If an API token is provided, we + * create a new API client with the token. Otherwise, we use the default API client. + */ + async function withAPI(fn: () => Promise): Promise { + // No token override - we can use the default API client. + if (!apiTokenOverride) { + return fn(); + } + + const client = await api(); + + if (!apiClient) { + // New client uses same endpoint and user agent as the default client. + apiClient = new GitBookAPI({ + endpoint: client.client.endpoint, + authToken: apiTokenOverride, + userAgent: client.client.userAgent, + }); + } + + return withAPIV1( + { + client: apiClient, + contextId: client.contextId, + }, + fn + ); + } + const dataFetcher: GitBookDataFetcher = { async api() { - const result = await api(); - return result.client; + return withAPI(async () => { + const result = await api(); + return result.client; + }); }, - withToken() { - // In v1, the token is global and controlled by the middleware. - // We don't need to do anything special here. - return dataFetcher; + withToken({ apiToken }) { + return getDataFetcherV1(apiToken); }, getUserById(userId) { - return wrapDataFetcherError(async () => { - const user = await getUserById(userId); - if (!user) { - throw new DataFetcherError('User not found', 404); - } - - return user; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const user = await getUserById(userId); + if (!user) { + throw new DataFetcherError('User not found', 404); + } + + return user; + }) + ); }, getPublishedContentSite(params) { - return wrapDataFetcherError(async () => { - return getPublishedContentSite(params); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getPublishedContentSite(params); + }) + ); }, getSpace(params) { - return wrapDataFetcherError(async () => { - return getSpace(params.spaceId, params.shareKey); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getSpace(params.spaceId, params.shareKey); + }) + ); }, getChangeRequest(params) { - return wrapDataFetcherError(async () => { - const changeRequest = await getChangeRequest( - params.spaceId, - params.changeRequestId - ); - if (!changeRequest) { - throw new DataFetcherError('Change request not found', 404); - } - - return changeRequest; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const changeRequest = await getChangeRequest( + params.spaceId, + params.changeRequestId + ); + if (!changeRequest) { + throw new DataFetcherError('Change request not found', 404); + } + + return changeRequest; + }) + ); }, getRevision(params) { - return wrapDataFetcherError(async () => { - return getRevision(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevision(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); + }) + ); }, getRevisionFile(params) { - return wrapDataFetcherError(async () => { - const revisionFile = await getRevisionFile( - params.spaceId, - params.revisionId, - params.fileId - ); - if (!revisionFile) { - throw new DataFetcherError('Revision file not found', 404); - } - - return revisionFile; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const revisionFile = await getRevisionFile( + params.spaceId, + params.revisionId, + params.fileId + ); + if (!revisionFile) { + throw new DataFetcherError('Revision file not found', 404); + } + + return revisionFile; + }) + ); }, getRevisionPageMarkdown() { @@ -158,117 +206,144 @@ async function getDataFetcherV1(): Promise { }, getDocument(params) { - return wrapDataFetcherError(async () => { - const document = await getDocument(params.spaceId, params.documentId); - if (!document) { - throw new DataFetcherError('Document not found', 404); - } - - return document; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const document = await getDocument(params.spaceId, params.documentId); + if (!document) { + throw new DataFetcherError('Document not found', 404); + } + + return document; + }) + ); }, getComputedDocument(params) { - return wrapDataFetcherError(() => { - return getComputedDocument( - params.organizationId, - params.spaceId, - params.source, - params.seed - ); - }); + return withAPI(() => + wrapDataFetcherError(() => { + return getComputedDocument( + params.organizationId, + params.spaceId, + params.source, + params.seed + ); + }) + ); }, getRevisionPages(params) { - return wrapDataFetcherError(async () => { - return getRevisionPages(params.spaceId, params.revisionId, { - metadata: params.metadata, - }); - }); + return withAPI(() => + wrapDataFetcherError(async () => { + return getRevisionPages(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); + }) + ); }, getRevisionPageByPath(params) { - return wrapDataFetcherError(async () => { - const revisionPage = await getRevisionPageByPath( - params.spaceId, - params.revisionId, - params.path - ); - - if (!revisionPage) { - throw new DataFetcherError('Revision page not found', 404); - } - - return revisionPage; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const revisionPage = await getRevisionPageByPath( + params.spaceId, + params.revisionId, + params.path + ); + + if (!revisionPage) { + throw new DataFetcherError('Revision page not found', 404); + } + + return revisionPage; + }) + ); }, getReusableContent(params) { - return wrapDataFetcherError(async () => { - const reusableContent = await getReusableContent( - params.spaceId, - params.revisionId, - params.reusableContentId - ); - - if (!reusableContent) { - throw new DataFetcherError('Reusable content not found', 404); - } - - return reusableContent; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const reusableContent = await getReusableContent( + params.spaceId, + params.revisionId, + params.reusableContentId + ); + + if (!reusableContent) { + throw new DataFetcherError('Reusable content not found', 404); + } + + return reusableContent; + }) + ); }, getLatestOpenAPISpecVersionContent(params) { - return wrapDataFetcherError(async () => { - const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent( - params.organizationId, - params.slug - ); - - if (!openAPISpecVersionContent) { - throw new DataFetcherError('OpenAPI spec version content not found', 404); - } - - return openAPISpecVersionContent; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent( + params.organizationId, + params.slug + ); + + if (!openAPISpecVersionContent) { + throw new DataFetcherError('OpenAPI spec version content not found', 404); + } + + return openAPISpecVersionContent; + }) + ); }, getSiteRedirectBySource(params) { - return wrapDataFetcherError(async () => { - const siteRedirect = await getSiteRedirectBySource(params); - if (!siteRedirect) { - throw new DataFetcherError('Site redirect not found', 404); - } - - return siteRedirect; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const siteRedirect = await getSiteRedirectBySource(params); + if (!siteRedirect) { + throw new DataFetcherError('Site redirect not found', 404); + } + + return siteRedirect; + }) + ); }, getEmbedByUrl(params) { - return wrapDataFetcherError(() => { - return getEmbedByUrlInSpace(params.spaceId, params.url); - }); + return withAPI(() => + wrapDataFetcherError(() => { + return getEmbedByUrlInSpace(params.spaceId, params.url); + }) + ); }, searchSiteContent(params) { - return wrapDataFetcherError(async () => { - const { organizationId, siteId, query, cacheBust, scope } = params; - const result = await searchSiteContent( - organizationId, - siteId, - query, - scope, - cacheBust - ); - return result.items; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const { organizationId, siteId, query, cacheBust, scope } = params; + const result = await searchSiteContent( + organizationId, + siteId, + query, + scope, + cacheBust + ); + return result.items; + }) + ); }, renderIntegrationUi(params) { - return wrapDataFetcherError(async () => { - const result = await renderIntegrationUi(params.integrationName, params.request); - return result; - }); + return withAPI(() => + wrapDataFetcherError(async () => { + const result = await renderIntegrationUi( + params.integrationName, + params.request + ); + return result; + }) + ); + }, + + streamAIResponse() { + throw new Error('Not implemented in v1'); }, }; @@ -325,3 +400,41 @@ export function getSitePointerFromContext(context: GitBookSiteContext): SiteCont siteShareKey: context.shareKey, }; } + +/** + * Return the base path for the current request. + * The value will start and finish with / + */ +async function getBasePath(): Promise { + assertIsNotV2(); + const headersList = await headers(); + const path = headersList.get('x-gitbook-basepath') ?? '/'; + + return withTrailingSlash(withLeadingSlash(path)); +} + +/** + * Return the site base path for the current request. + * The value will start and finish with / + */ +async function getSiteBasePath(): Promise { + assertIsNotV2(); + const headersList = await headers(); + const path = headersList.get('x-gitbook-site-basepath') ?? '/'; + + return withTrailingSlash(withLeadingSlash(path)); +} + +/** + * Return the current host for the current request. + */ +async function getHost(): Promise { + assertIsNotV2(); + const headersList = await headers(); + const mode = headersList.get('x-gitbook-mode'); + if (mode === 'proxy') { + return headersList.get('x-forwarded-host') ?? ''; + } + + return headersList.get('x-gitbook-host') ?? headersList.get('host') ?? ''; +} diff --git a/packages/gitbook/src/lib/visitor-token.test.ts b/packages/gitbook/src/lib/visitors.test.ts similarity index 56% rename from packages/gitbook/src/lib/visitor-token.test.ts rename to packages/gitbook/src/lib/visitors.test.ts index a1f07d250..4337371fa 100644 --- a/packages/gitbook/src/lib/visitor-token.test.ts +++ b/packages/gitbook/src/lib/visitors.test.ts @@ -6,7 +6,8 @@ import { getVisitorAuthCookieName, getVisitorAuthCookieValue, getVisitorToken, -} from './visitor-token'; + getVisitorUnsignedClaims, +} from './visitors'; describe('getVisitorAuthToken', () => { it('should return the token from the query parameters', () => { @@ -158,3 +159,139 @@ function assertVisitorAuthCookieValue( throw new Error('Expected a VisitorAuthCookieValue'); } + +describe('getVisitorUnsignedClaims', () => { + it('should merge claims from multiple public cookies', () => { + const cookies = [ + { + name: 'gitbook-visitor-public-bucket', + value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }), + }, + { + name: 'gitbook-visitor-public-launchdarkly', + value: JSON.stringify({ + launchdarkly: { flags: { ALPHA: true, API: true } }, + }), + }, + ]; + + const url = new URL('https://example.com/'); + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims.all).toStrictEqual({ + bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } }, + launchdarkly: { flags: { ALPHA: true, API: true } }, + }); + }); + + it('should parse visitor.* query params with simple types', () => { + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.language=fr&visitor.country=fr' + ); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims.all).toStrictEqual({ + isEnterprise: true, + language: 'fr', + country: 'fr', + }); + expect(claims.fromVisitorParams).toStrictEqual({ + isEnterprise: true, + language: 'fr', + country: 'fr', + }); + }); + + it('should ignore params that do not match visitor.* convention', () => { + const url = new URL('https://example.com/?visitor.isEnterprise=true&otherParam=true'); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims.all).toStrictEqual({ + isEnterprise: true, + // otherParam is not present + }); + expect(claims.fromVisitorParams).toStrictEqual({ + isEnterprise: true, + }); + }); + + it('should support nested query param keys via dot notation', () => { + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false' + ); + + const claims = getVisitorUnsignedClaims({ cookies: [], url }); + + expect(claims.all).toStrictEqual({ + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + expect(claims.fromVisitorParams).toStrictEqual({ + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + }); + + it('should ignore invalid JSON in cookie values', () => { + const cookies = [ + { + name: 'gitbook-visitor-public', + value: '{not: "json"}', + }, + ]; + const url = new URL('https://example.com/'); + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims.all).toStrictEqual({}); + }); + + it('should merge claims from cookies and visitor.* query params', () => { + const cookies = [ + { + name: 'gitbook-visitor-public', + value: JSON.stringify({ role: 'admin', language: 'fr' }), + }, + { + name: 'gitbook-visitor-public-bucket', + value: JSON.stringify({ bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } } }), + }, + ]; + const url = new URL( + 'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false' + ); + + const claims = getVisitorUnsignedClaims({ cookies, url }); + + expect(claims.all).toStrictEqual({ + role: 'admin', + language: 'fr', + bucket: { + flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true }, + }, + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + + expect(claims.fromVisitorParams).toStrictEqual({ + bucket: { + flags: { HELLO: false }, + }, + isEnterprise: true, + flags: { + ALPHA: true, + API: false, + }, + }); + }); +}); diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitors.ts similarity index 61% rename from packages/gitbook/src/lib/visitor-token.ts rename to packages/gitbook/src/lib/visitors.ts index 64f0c9793..7ae970666 100644 --- a/packages/gitbook/src/lib/visitor-token.ts +++ b/packages/gitbook/src/lib/visitors.ts @@ -4,6 +4,7 @@ import hash from 'object-hash'; const VISITOR_AUTH_PARAM = 'jwt_token'; export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token'; +const VISITOR_UNSIGNED_CLAIMS_PREFIX = 'gitbook-visitor-public'; /** * Typing for a cookie, matching the internal type of Next.js. @@ -30,6 +31,27 @@ type VisitorAuthCookieValue = { token: string; }; +type ClaimPrimitive = + | string + | number + | boolean + | null + | undefined + | { [key: string]: ClaimPrimitive } + | ClaimPrimitive[]; + +/** + * The result of a visitor data lookup that can include: + * - a visitor token (JWT) + * - a record of visitor public/unsigned claims (JSON object) + * - a session cookie response to persist any visitor query params across navigations. + */ +export type VisitorDataLookup = { + visitorToken: VisitorTokenLookup; + unsignedClaims: Record; + visitorParamsCookie: ResponseCookie | undefined; +}; + /** * The result of a visitor token lookup. */ @@ -53,6 +75,30 @@ export type VisitorTokenLookup = /** Not visitor token was found */ | undefined; +/** + * Get the visitor data for the request potentially including: + * - a JWT token that may contain signed claims or can be used for VA authentication. + * - a record of the unsigned claims passed via a cookie or visitor.* params. + * - a session cookie response that is used to persist any visitor.* params that were passed via the site URL. + */ +export function getVisitorData({ + cookies, + url, +}: { + cookies: RequestCookies; + url: URL | NextRequest['nextUrl']; +}): VisitorDataLookup { + const visitorToken = getVisitorToken({ cookies, url }); + const unsignedClaims = getVisitorUnsignedClaims({ cookies, url }); + const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams); + + return { + visitorToken, + unsignedClaims: unsignedClaims.all, + visitorParamsCookie, + }; +} + /** * Get the visitor token for the request. This token can either be in the * query parameters or stored as a cookie. @@ -82,6 +128,138 @@ export function getVisitorToken({ } } +/** + * Get the visitor unsigned/public claims for the request. They can either be in `visitor.` query + * parameters or stored in special `gitbook-visitor-public-*` cookies. + */ +export function getVisitorUnsignedClaims(args: { + cookies: RequestCookies; + url: URL | NextRequest['nextUrl']; +}): { + /** + * The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params. + */ + all: Record; + /** + * The unsigned claims from the `visitor.*` query params. + */ + fromVisitorParams: Record; +} { + const { cookies, url } = args; + const claims: Record = {}; + const searchParamsClaims: Record = {}; + + for (const cookie of cookies) { + if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) { + try { + const parsed = JSON.parse(cookie.value); + if (typeof parsed === 'object' && parsed !== null) { + Object.assign(claims, parsed); + } + } catch (_err) { + console.warn(`Invalid JSON in unsigned claim cookie "${cookie.name}"`); + } + } + } + + for (const [key, value] of url.searchParams.entries()) { + if (key.startsWith('visitor.')) { + const claimPath = key.substring('visitor.'.length); + const claimValue = parseVisitorQueryParamValue(value); + + setVisitorClaimByPath(claims, claimPath, claimValue); + setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue); + } + } + + return { all: claims, fromVisitorParams: searchParamsClaims }; +} + +/** + * Set the value of claims in a claims object at a specific path. + */ +function setVisitorClaimByPath( + claims: Record, + keyPath: string, + value: ClaimPrimitive +): void { + const keys = keyPath.split('.'); + let current = claims; + + for (let index = 0; index < keys.length; index++) { + const key = keys[index]; + + if (index === keys.length - 1) { + current[key] = value; + } else { + if (!(key in current) || !isClaimPrimitiveObject(current[key])) { + current[key] = {}; + } + + current = current[key]; + } + } +} + +function isClaimPrimitiveObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +/** + * Parse the value expected in a `visitor.` URL query parameter. + */ +function parseVisitorQueryParamValue(value: string): ClaimPrimitive { + if (value === 'true') { + return true; + } + + if (value === 'false') { + return false; + } + + if (value === 'null') { + return null; + } + + if (value === 'undefined') { + return undefined; + } + + const num = Number(value); + if (!Number.isNaN(num) && value.trim() !== '') { + return num; + } + + try { + const parsed = JSON.parse(value); + if (typeof parsed === 'object' && parsed !== null) { + return parsed; + } + } catch {} + + return value; +} + +/** + * Returns to cookie response to use in order to persist visitor params that were passed to the URL. + */ +function getResponseCookieForVisitorParams( + visitorParamsClaims: Record +): ResponseCookie | undefined { + if (Object.keys(visitorParamsClaims).length === 0) { + return undefined; + } + + return { + name: VISITOR_UNSIGNED_CLAIMS_PREFIX, + value: JSON.stringify(visitorParamsClaims), + options: { + sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined, + secure: process.env.NODE_ENV === 'production', + }, + }; +} + /** * Return the lookup result for content served with visitor auth. */ diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 30f7b44e6..3e5f682cc 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -24,9 +24,9 @@ import { type ResponseCookies, type VisitorTokenLookup, getResponseCookiesForVisitorAuth, - getVisitorToken, + getVisitorData, normalizeVisitorAuthURL, -} from '@/lib/visitor-token'; +} from '@/lib/visitors'; import { joinPath, withLeadingSlash } from '@/lib/paths'; import { getProxyModeBasePath } from '@/lib/proxy'; @@ -392,17 +392,17 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise { - const visitorAuthToken = getVisitorToken({ + const { visitorToken } = getVisitorData({ cookies: request.cookies.getAll(), url, }); - const lookup = await lookupSiteByAPI(url, visitorAuthToken); + const lookup = await lookupSiteByAPI(url, visitorToken); return { ...lookup, - ...('basePath' in lookup && visitorAuthToken - ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken) + ...('basePath' in lookup && visitorToken + ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken) : {}), - visitorToken: visitorAuthToken?.token, + visitorToken: visitorToken?.token, }; } @@ -609,12 +609,12 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis const target = new URL(targetStr); target.search = url.search; - const visitorAuthToken = getVisitorToken({ + const { visitorToken } = getVisitorData({ cookies: request.cookies.getAll(), url: target, }); - const lookup = await lookupSiteByAPI(target, visitorAuthToken); + const lookup = await lookupSiteByAPI(target, visitorToken); if ('error' in lookup) { return lookup; } @@ -641,10 +641,10 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis ...lookup, siteBasePath: joinPath(target.host, lookup.siteBasePath), basePath: joinPath(target.host, lookup.basePath), - ...('basePath' in lookup && visitorAuthToken - ? getLookupResultForVisitorAuth(lookup.basePath, visitorAuthToken) + ...('basePath' in lookup && visitorToken + ? getLookupResultForVisitorAuth(lookup.basePath, visitorToken) : {}), - visitorToken: visitorAuthToken?.token, + visitorToken: visitorToken?.token, }; } diff --git a/packages/gitbook/src/routes/icon.tsx b/packages/gitbook/src/routes/icon.tsx index 425bb4b84..03a6f3522 100644 --- a/packages/gitbook/src/routes/icon.tsx +++ b/packages/gitbook/src/routes/icon.tsx @@ -3,6 +3,7 @@ import { ImageResponse } from 'next/og'; import { getEmojiForCode } from '@/lib/emojis'; import { tcls } from '@/lib/tailwind'; +import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; import { getResizedImageURL } from '@v2/lib/images'; @@ -73,6 +74,14 @@ export async function serveIcon(context: GitBookSiteContext, req: Request) { { width: size.width, height: size.height, + headers: { + 'cache-tag': [ + getCacheTag({ + tag: 'site', + site: context.site.id, + }), + ].join(','), + }, } ); } diff --git a/packages/gitbook/src/routes/image.ts b/packages/gitbook/src/routes/image.ts index fb4d7ec89..3bc946ed9 100644 --- a/packages/gitbook/src/routes/image.ts +++ b/packages/gitbook/src/routes/image.ts @@ -2,6 +2,7 @@ import { CURRENT_SIGNATURE_VERSION, type CloudflareImageOptions, type SignatureVersion, + SizableImageAction, checkIsSizableImageURL, isSignatureVersion, parseImageAPIURL, @@ -40,7 +41,7 @@ export async function serveResizedImage( // Check again if the image can be sized, even though we checked when rendering the Image component // Otherwise, it's possible to pass just any link to this endpoint and trigger HTML injection on the domain // Also prevent infinite redirects. - if (!checkIsSizableImageURL(url)) { + if (checkIsSizableImageURL(url) === SizableImageAction.Skip) { return new Response('Invalid url parameter', { status: 400 }); } @@ -107,12 +108,13 @@ export async function serveResizedImage( try { const response = await resizeImage(url, options); if (!response.ok) { - throw new Error('Failed to resize image'); + throw new Error(`Failed to resize image, received status code ${response.status}`); } return response; - } catch (_error) { + } catch (error) { // Redirect to the original image if resizing fails + console.warn('Error while resizing image, redirecting to original', error); return NextResponse.redirect(url, 302); } } diff --git a/packages/gitbook/src/routes/ogimage.tsx b/packages/gitbook/src/routes/ogimage.tsx index 59a7a66f1..ee586cb1b 100644 --- a/packages/gitbook/src/routes/ogimage.tsx +++ b/packages/gitbook/src/routes/ogimage.tsx @@ -7,6 +7,7 @@ import { type PageParams, fetchPageData } from '@/components/SitePage'; import { getFontSourcesToPreload } from '@/fonts/custom'; import { getAssetURL } from '@/lib/assets'; import { filterOutNullable } from '@/lib/typescript'; +import { getCacheTag } from '@gitbook/cache-tags'; import type { GitBookSiteContext } from '@v2/lib/context'; import { getResizedImageURL } from '@v2/lib/images'; @@ -87,12 +88,15 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page await Promise.all( primaryFontWeights.map((face) => { const { weight, sources } = face; - if (sources.length === 0) { + const source = sources[0]; + + // Satori doesn't support WOFF2, so we skip it + // https://github.com/vercel/satori?tab=readme-ov-file#fonts + if (!source || source.format === 'woff2' || source.url.endsWith('.woff2')) { return null; } - const url = sources[0].url; - return loadCustomFont({ url, weight }); + return loadCustomFont({ url: source.url, weight }); }) ) ).filter(filterOutNullable); @@ -169,8 +173,12 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page {String.fromCodePoint(Number.parseInt(`0x${customization.favicon.emoji}`))} ); - const src = linker.toAbsoluteURL( - linker.toPathInSpace(`~gitbook/icon?size=medium&theme=${customization.themes.default}`) + const src = await readSelfImage( + linker.toAbsoluteURL( + linker.toPathInSpace( + `~gitbook/icon?size=medium&theme=${customization.themes.default}` + ) + ) ); return Icon; })(); @@ -192,7 +200,11 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page /> {/* Grid */} - Grid + Grid {/* Logo */} {customization.header.logo ? ( @@ -228,6 +240,18 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page width: 1200, height: 630, fonts: fonts.length ? fonts : undefined, + headers: { + 'cache-tag': [ + getCacheTag({ + tag: 'site', + site: baseContext.site.id, + }), + getCacheTag({ + tag: 'space', + space: baseContext.space.id, + }), + ].join(','), + }, } ); } @@ -285,3 +309,57 @@ async function loadCustomFont(input: { url: string; weight: 400 | 700 }) { weight, }; } + +/** + * Temporary function to log some data on Cloudflare. + * TODO: remove this when we found the issue + */ +function logOnCloudflareOnly(message: string) { + if (process.env.DEBUG_CLOUDFLARE === 'true') { + // biome-ignore lint/suspicious/noConsole: + console.log(message); + } +} + +/** + * Read an image from a response as a base64 encoded string. + */ +async function readImage(response: Response) { + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.startsWith('image/')) { + logOnCloudflareOnly(`Invalid content type: ${contentType}, + status: ${response.status} + rayId: ${response.headers.get('cf-ray')}`); + throw new Error(`Invalid content type: ${contentType}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const base64 = Buffer.from(arrayBuffer).toString('base64'); + return `data:${contentType};base64,${base64}`; +} + +const staticImagesCache = new Map(); + +/** + * Read a static image and cache it in memory. + */ +async function readStaticImage(url: string) { + logOnCloudflareOnly(`Reading static image: ${url}, cache size: ${staticImagesCache.size}`); + const cached = staticImagesCache.get(url); + if (cached) { + return cached; + } + + const image = await readSelfImage(url); + staticImagesCache.set(url, image); + return image; +} + +/** + * Read an image from GitBook itself. + */ +async function readSelfImage(url: string) { + const response = await fetch(url); + const image = await readImage(response); + return image; +} diff --git a/packages/gitbook/src/routes/robots.ts b/packages/gitbook/src/routes/robots.ts index 297fc4677..1e1c0a287 100644 --- a/packages/gitbook/src/routes/robots.ts +++ b/packages/gitbook/src/routes/robots.ts @@ -8,20 +8,23 @@ export async function serveRobotsTxt(context: GitBookSiteContext) { const { linker } = context; const isRoot = checkIsRootSiteContext(context); - const lines = [ - 'User-agent: *', - // Disallow dynamic routes / search queries - 'Disallow: /*?', - ...((await isSiteIndexable(context)) - ? [ - 'Allow: /', - `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`, - ] - : ['Disallow: /']), - ]; - const content = lines.join('\n'); + const isIndexable = await isSiteIndexable(context); - return new Response(content, { + const lines = isIndexable + ? [ + 'User-agent: *', + // Allow image resizing and icon generation routes for favicons and search results + 'Allow: /~gitbook/image?*', + 'Allow: /~gitbook/icon?*', + // Disallow other dynamic routes / search queries + 'Disallow: /*?', + 'Allow: /', + `Sitemap: ${linker.toAbsoluteURL(linker.toPathInSpace(isRoot ? '/sitemap.xml' : '/sitemap-pages.xml'))}`, + ] + : ['User-agent: *', 'Disallow: /']; + + const robotsTxt = lines.join('\n'); + return new Response(robotsTxt, { headers: { 'Content-Type': 'text/plain', }, diff --git a/packages/gitbook/src/routes/sitemap.ts b/packages/gitbook/src/routes/sitemap.ts index 4a64e1238..a5f5e452f 100644 --- a/packages/gitbook/src/routes/sitemap.ts +++ b/packages/gitbook/src/routes/sitemap.ts @@ -141,7 +141,7 @@ function getUrlsFromSiteSpaces(context: GitBookSiteContext, siteSpaces: SiteSpac } const url = new URL(siteSpace.urls.published); url.pathname = joinPath(url.pathname, 'sitemap-pages.xml'); - return context.linker.toLinkForContent(url.toString()); + return context.linker.toAbsoluteURL(context.linker.toLinkForContent(url.toString())); }, []); return urls.filter(filterOutNullable); } diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 692487f09..9d248c9a4 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -295,7 +295,7 @@ const config: Config = { ), }, animation: { - present: 'present .5s ease-out both', + present: 'present 200ms cubic-bezier(0.25, 1, 0.5, 1) both', scaleIn: 'scaleIn 200ms ease', scaleOut: 'scaleOut 200ms ease', fadeIn: 'fadeIn 200ms ease forwards', @@ -330,7 +330,7 @@ const config: Config = { present: { from: { opacity: '0', - transform: 'translateY(2rem) scale(0.9)', + transform: 'translateY(1rem) scale(90%)', }, to: { opacity: '1', @@ -443,6 +443,7 @@ const config: Config = { scale: { '98': '0.98', '102': '1.02', + '104': '1.04', }, }, opacity: opacity(), @@ -457,12 +458,16 @@ const config: Config = { /** * Variant when a header is displayed. */ - addVariant('site-header-none', 'html.site-header-none &'); + addVariant('site-header-none', 'body:not(:has(#site-header:not(.mobile-only))) &'); addVariant('site-header', 'body:has(#site-header:not(.mobile-only)) &'); addVariant('site-header-sections', [ 'body:has(#site-header:not(.mobile-only) #sections) &', 'body:has(.page-no-toc):has(#site-header:not(.mobile-only) #variants) &', ]); + addVariant( + 'announcement', + 'html:not(.announcement-hidden):has(#announcement-banner) &' + ); const customisationVariants = { // Sidebar styles diff --git a/packages/gitbook/tests/pagespeed-testing.ts b/packages/gitbook/tests/pagespeed-testing.ts index 94f48d420..07f1f01b3 100644 --- a/packages/gitbook/tests/pagespeed-testing.ts +++ b/packages/gitbook/tests/pagespeed-testing.ts @@ -12,13 +12,13 @@ interface Test { // and to be able to see the results, and only catch major regressions. const tests: Array = [ { - url: 'https://docs.gitbook.com', + url: 'https://gitbook.com/docs', strategy: 'desktop', threshold: 60, }, { - url: 'https://docs.gitbook.com', + url: 'https://gitbook.com/docs', strategy: 'mobile', threshold: 30, }, diff --git a/packages/icons/bin/gen-list.js b/packages/icons/bin/gen-list.js index acd734ffe..996bf091f 100644 --- a/packages/icons/bin/gen-list.js +++ b/packages/icons/bin/gen-list.js @@ -54,6 +54,9 @@ async function main() { writeDataFile('styles-map', JSON.stringify(onlyStyles, null, 2)), writeDataFile('icons', JSON.stringify(result, null, 2)), ]); + + // biome-ignore lint/suspicious/noConsole: We want the CLI to log + console.log(`Generated ${result.length} icons`); } async function writeDataFile(name, content) { @@ -74,6 +77,7 @@ async function writeDataFile(name, content) { ]); } -main().catch((_error) => { +main().catch((error) => { + console.error(`Error generating icons list: ${error}`); process.exit(1); }); diff --git a/packages/icons/bin/gitbook-icons.js b/packages/icons/bin/gitbook-icons.js index 5dbdaeb51..03446a15a 100755 --- a/packages/icons/bin/gitbook-icons.js +++ b/packages/icons/bin/gitbook-icons.js @@ -20,8 +20,6 @@ async function main() { // Create the output folder if it doesn't exist await fs.mkdir(outputFolder, { recursive: true }); - const _printOutputFolder = path.relative(process.cwd(), outputFolder); - // Copy the assets from // source/sprites to outputFolder/sprites // source/svgs to outputFolder/svgs @@ -45,8 +43,12 @@ async function main() { } }), ]); + + // biome-ignore lint/suspicious/noConsole: We want the CLI to log + console.log(`Copied ${stylesToCopy.length} styles to ${outputFolder}`); } -main().catch((_error) => { +main().catch((error) => { + console.error(`Error copying icons: ${error}`); process.exit(1); }); diff --git a/packages/icons/turbo.json b/packages/icons/turbo.json index 1b43f537d..9097cda33 100644 --- a/packages/icons/turbo.json +++ b/packages/icons/turbo.json @@ -2,7 +2,8 @@ "extends": ["//"], "tasks": { "generate": { - "outputs": ["data/*.json"] + "inputs": ["bin/**/*", "package.json"], + "outputs": ["src/data/*.json", "dist/data/*.json"] } } } diff --git a/packages/openapi-parser/CHANGELOG.md b/packages/openapi-parser/CHANGELOG.md index f1fc4bd53..7f6ac7ccd 100644 --- a/packages/openapi-parser/CHANGELOG.md +++ b/packages/openapi-parser/CHANGELOG.md @@ -1,5 +1,17 @@ # @gitbook/openapi-parser +## 2.1.4 + +### Patch Changes + +- d00dc8c: Pass scalar's errors through OpenAPIParseError + +## 2.1.3 + +### Patch Changes + +- 2b6c593: Remove stable from x-stability + ## 2.1.2 ### Patch Changes diff --git a/packages/openapi-parser/package.json b/packages/openapi-parser/package.json index 63fc45ffc..e65ebb4dd 100644 --- a/packages/openapi-parser/package.json +++ b/packages/openapi-parser/package.json @@ -9,7 +9,7 @@ "default": "./dist/index.js" } }, - "version": "2.1.2", + "version": "2.1.4", "sideEffects": false, "dependencies": { "@scalar/openapi-parser": "^0.10.10", diff --git a/packages/openapi-parser/src/error.ts b/packages/openapi-parser/src/error.ts index e0d25d3e2..a7c5398bc 100644 --- a/packages/openapi-parser/src/error.ts +++ b/packages/openapi-parser/src/error.ts @@ -1,3 +1,5 @@ +import type { ErrorObject } from '@scalar/openapi-parser'; + type OpenAPIParseErrorCode = | 'invalid' | 'parse-v2-in-v3' @@ -12,17 +14,19 @@ export class OpenAPIParseError extends Error { public override name = 'OpenAPIParseError'; public code: OpenAPIParseErrorCode; public rootURL: string | null; - + public errors: ErrorObject[] | undefined; constructor( message: string, options: { code: OpenAPIParseErrorCode; rootURL?: string | null; cause?: Error; + errors?: ErrorObject[] | undefined; } ) { super(message, { cause: options.cause }); this.code = options.code; this.rootURL = options.rootURL ?? null; + this.errors = options.errors; } } diff --git a/packages/openapi-parser/src/types.ts b/packages/openapi-parser/src/types.ts index 6c72c284c..fa5b6ade3 100644 --- a/packages/openapi-parser/src/types.ts +++ b/packages/openapi-parser/src/types.ts @@ -65,12 +65,12 @@ export interface OpenAPICustomOperationProperties { /** * Stability of the operation. - * @enum 'experimental' | 'alpha' | 'beta' | 'stable' + * @enum 'experimental' | 'alpha' | 'beta' */ 'x-stability'?: OpenAPIStability; } -export type OpenAPIStability = 'experimental' | 'alpha' | 'beta' | 'stable'; +export type OpenAPIStability = 'experimental' | 'alpha' | 'beta'; /** * Custom code samples that can be defined at the operation level. diff --git a/packages/openapi-parser/src/v3.ts b/packages/openapi-parser/src/v3.ts index 14a890769..3b54819cf 100644 --- a/packages/openapi-parser/src/v3.ts +++ b/packages/openapi-parser/src/v3.ts @@ -40,6 +40,7 @@ async function untrustedValidate(input: ValidateOpenAPIV3Input) { throw new OpenAPIParseError('Invalid OpenAPI document', { code: 'invalid', rootURL, + errors: result.errors, }); } diff --git a/packages/proxy/.gitignore b/packages/proxy/.gitignore deleted file mode 100644 index 849ddff3b..000000000 --- a/packages/proxy/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/packages/proxy/CHANGELOG.md b/packages/proxy/CHANGELOG.md deleted file mode 100644 index 308869fa5..000000000 --- a/packages/proxy/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @gitbook/proxy - -## 0.1.0 - -### Minor Changes - -- 53b9f10: First version diff --git a/packages/proxy/README.md b/packages/proxy/README.md deleted file mode 100644 index e9e609601..000000000 --- a/packages/proxy/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# `@gitbook/proxy` - -Host a GitBook site on your own domain as a subpath. - -## Usage - -```ts -import { proxyToGitBook } from '@gitbook/proxy'; - -const site = proxyToGitBook(event.request, { - site: 'mycompany.gitbook.io/site/', - basePath: '/docs', -}); - -export default { - async fetch(request) { - // If the requst matches the basePath /docs, we serve from GitBook - if (site.match(request)) { - return site.fetch(request); - } - - // Otherwise we do something else. - return new Response('Not found', { - statusCode: 404, - }); - }, -}; -``` diff --git a/packages/proxy/package.json b/packages/proxy/package.json deleted file mode 100644 index 796ac7d96..000000000 --- a/packages/proxy/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@gitbook/proxy", - "description": "Host a GitBook site on your own domain as a subpath", - "version": "0.1.0", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "development": "./src/index.ts", - "default": "./dist/index.js" - } - }, - "dependencies": {}, - "devDependencies": { - "typescript": "^5.5.3" - }, - "scripts": { - "build": "tsc", - "typecheck": "tsc --noEmit", - "clean": "rm -rf ./dist", - "unit": "bun test" - }, - "files": ["dist", "src", "README.md", "CHANGELOG.md"] -} diff --git a/packages/proxy/src/index.test.ts b/packages/proxy/src/index.test.ts deleted file mode 100644 index 81fa218a1..000000000 --- a/packages/proxy/src/index.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from 'bun:test'; -import { proxyToGitBook } from '.'; - -describe('.match', () => { - it('should return true if the request is below the base path', () => { - const site = proxyToGitBook({ site: 'https://org.gitbook.io/example/', basePath: '/docs' }); - expect(site.match('/docs')).toBe(true); - expect(site.match('/docs/')).toBe(true); - expect(site.match('/docs/hello')).toBe(true); - expect(site.match('/docs/hello/world')).toBe(true); - - expect(site.match('/hello/world')).toBe(false); - expect(site.match('/')).toBe(false); - }); -}); - -describe('.request', () => { - it('should compute a proper request for a sub-path', () => { - const site = proxyToGitBook({ site: 'https://org.gitbook.io/example/', basePath: '/docs' }); - const request = new Request('https://example.com/docs/hello/world'); - - const proxiedRequest = site.request(request); - expect(proxiedRequest.url).toBe('https://hosting.gitbook.io/docs/hello/world'); - expect(proxiedRequest.headers.get('Host')).toBe('hosting.gitbook.io'); - expect(proxiedRequest.headers.get('X-Forwarded-Host')).toBe('example.com'); - expect(proxiedRequest.headers.get('X-GitBook-BasePath')).toBe('/docs'); - expect(proxiedRequest.headers.get('X-GitBook-Site-URL')).toBe( - 'https://org.gitbook.io/example/' - ); - }); - - it('should compute a proper request on the root', () => { - const site = proxyToGitBook({ site: 'https://org.gitbook.io/example/', basePath: '/docs' }); - const request = new Request('https://example.com/docs'); - - const proxiedRequest = site.request(request); - expect(proxiedRequest.url).toBe('https://hosting.gitbook.io/docs'); - expect(proxiedRequest.headers.get('Host')).toBe('hosting.gitbook.io'); - expect(proxiedRequest.headers.get('X-Forwarded-Host')).toBe('example.com'); - expect(proxiedRequest.headers.get('X-GitBook-BasePath')).toBe('/docs'); - expect(proxiedRequest.headers.get('X-GitBook-Site-URL')).toBe( - 'https://org.gitbook.io/example/' - ); - }); - - it('should normalize the basepath', () => { - const site = proxyToGitBook({ site: 'https://org.gitbook.io/example/', basePath: 'docs/' }); - const request = new Request('https://example.com/docs/hello/world'); - - const proxiedRequest = site.request(request); - expect(proxiedRequest.url).toBe('https://hosting.gitbook.io/docs/hello/world'); - expect(proxiedRequest.headers.get('Host')).toBe('hosting.gitbook.io'); - expect(proxiedRequest.headers.get('X-Forwarded-Host')).toBe('example.com'); - expect(proxiedRequest.headers.get('X-GitBook-BasePath')).toBe('/docs'); - expect(proxiedRequest.headers.get('X-GitBook-Site-URL')).toBe( - 'https://org.gitbook.io/example/' - ); - }); -}); diff --git a/packages/proxy/src/index.ts b/packages/proxy/src/index.ts deleted file mode 100644 index 1d960637f..000000000 --- a/packages/proxy/src/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -export interface ProxyToGitBookOptions { - /** - * The URL of the published site. - * @example "https://mycompany.gitbook.io/docs" - */ - site: string; - - /** - * Base path to serve the site on. - * @example "/docs" - */ - basePath: string; - - /** - * Hostname used by GitBook to serve content. - * Do not set this option unless you know what you are doing. - */ - gitbookHost?: string; -} - -export interface ProxySite { - /** - * Test if the request should be proxied to this site. - */ - match(request: Request | string): boolean; - - /** - * Get the proxied request for a given request. - */ - request(request: Request): Request; - - /** - * Fetch the request from the site. - */ - fetch(request: Request): Promise; -} - -/** - * Proxies requests to a GitBook site. - */ -export function proxyToGitBook(options: ProxyToGitBookOptions): ProxySite { - const { gitbookHost = 'hosting.gitbook.io' } = options; - - const siteUrl = new URL(options.site); - const rawSiteUrl = siteUrl.toString(); - - const basePath = normalizeBasePath(options.basePath); - - const site: ProxySite = { - match: (request) => { - const pathname = typeof request === 'string' ? request : new URL(request.url).pathname; - return pathname === basePath || pathname.startsWith(`${basePath}/`); - }, - - request: (originRequest) => { - const originUrl = new URL(originRequest.url); - - const url = new URL(originUrl); - url.hostname = gitbookHost; - - const proxyRequest = new Request(url, originRequest); - proxyRequest.headers.set('Host', gitbookHost); - - // Pass the original host and protocol - proxyRequest.headers.set('X-Forwarded-Host', originUrl.hostname); - proxyRequest.headers.set('X-Forwarded-Proto', 'https'); - - // Pass the basepath on the original URL - proxyRequest.headers.set('X-GitBook-BasePath', basePath); - - // Pass the site URL - proxyRequest.headers.set('X-GitBook-Site-URL', rawSiteUrl); - - return proxyRequest; - }, - - fetch: async (originRequest) => { - return fetch(site.request(originRequest)); - }, - }; - - return site; -} - -function normalizeBasePath(basePath: string): string { - let result = withLeadingSlash(basePath); - result = withoutTrailingSlash(result); - return result; -} - -function withLeadingSlash(path: string): string { - return path.startsWith('/') ? path : `/${path}`; -} - -function withoutTrailingSlash(path: string): string { - return path.endsWith('/') ? path.slice(0, -1) : path; -} diff --git a/packages/proxy/tsconfig.json b/packages/proxy/tsconfig.json deleted file mode 100644 index aa63c0ef4..000000000 --- a/packages/proxy/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "esnext", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": false, - "declaration": true, - "outDir": "dist", - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "incremental": true, - "types": [ - "bun-types" // add Bun global - ] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/react-contentkit/package.json b/packages/react-contentkit/package.json index d2c8bd8ee..09e6cc8e7 100644 --- a/packages/react-contentkit/package.json +++ b/packages/react-contentkit/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "classnames": "^2.5.1", - "@gitbook/api": "*", + "@gitbook/api": "^0.115.0", "@gitbook/icons": "workspace:*" }, "peerDependencies": { diff --git a/packages/react-openapi/CHANGELOG.md b/packages/react-openapi/CHANGELOG.md index d977f51c3..6f36c7dc6 100644 --- a/packages/react-openapi/CHANGELOG.md +++ b/packages/react-openapi/CHANGELOG.md @@ -1,5 +1,66 @@ # @gitbook/react-openapi +## 1.3.0 + +### Minor Changes + +- 326e28e: Design tweaks to code blocks and OpenAPI pages + +### Patch Changes + +- 42ca7e1: Fix openapi CR preview +- 5e975ab: Fix code highlighting for HTTP +- 580101d: Fix schemas disclosure label causing client error +- 20ebecb: Missing top-level required OpenAPI alternatives +- 80cb52a: Handle OpenAPI alternatives from schema.items +- cb5598d: Handle invalid OpenAPI Responses +- c6637b0: Use default value if string number or boolean in generateSchemaExample +- a3ec264: Fix Python code sample "null vs None" +- Updated dependencies [d00dc8c] + - @gitbook/openapi-parser@2.1.4 + +## 1.2.1 + +### Patch Changes + +- ebc39e9: Missing select icon +- b6b09d4: Fix OpenAPI responses select placement and icon + +## 1.2.0 + +### Minor Changes + +- d67699a: Add OpenAPI Webhook block + +### Patch Changes + +- eeb977f: Fix Python code example for JSON payload +- 3363a18: Merge simple alternatives +- 8ed1bda: Translate OpenAPI blocks +- 7588cfe: Improve OpenAPIResponses examples and schemas +- ad1dc0b: Bump scalar packages + +## 1.1.10 + +### Patch Changes + +- 70c4182: Improve OpenAPI schema style +- 2b6c593: Remove stable from x-stability +- cbd768a: Improve OpenAPI codesample (add OpenAPISelect component) +- e59076a: Improve OpenAPI schemas block ungrouped style. Classnames have changed, please refer to this PR to update GBX. +- eedefdd: Handle optional security headers +- 23cedd2: Hide deprecated properties in examples +- Updated dependencies [2b6c593] + - @gitbook/openapi-parser@2.1.3 + +## 1.1.9 + +### Patch Changes + +- da7b369: Fix missing headers in OpenAPIResponses +- da485f5: Fix read-only in generateSchemaExample +- 139a805: Fix OpenAPI enum display + ## 1.1.8 ### Patch Changes diff --git a/packages/react-openapi/package.json b/packages/react-openapi/package.json index d1f2b29ef..d4f170270 100644 --- a/packages/react-openapi/package.json +++ b/packages/react-openapi/package.json @@ -8,12 +8,12 @@ "default": "./dist/index.js" } }, - "version": "1.1.8", + "version": "1.3.0", "sideEffects": false, "dependencies": { "@gitbook/openapi-parser": "workspace:*", - "@scalar/api-client-react": "^1.2.5", - "@scalar/oas-utils": "^0.2.120", + "@scalar/api-client-react": "^1.2.19", + "@scalar/oas-utils": "^0.2.130", "clsx": "^2.1.1", "flatted": "^3.2.9", "json-xml-parse": "^1.3.0", diff --git a/packages/react-openapi/src/InteractiveSection.tsx b/packages/react-openapi/src/InteractiveSection.tsx index 255ff53f0..db8a2662c 100644 --- a/packages/react-openapi/src/InteractiveSection.tsx +++ b/packages/react-openapi/src/InteractiveSection.tsx @@ -1,9 +1,10 @@ 'use client'; import clsx from 'clsx'; -import { useRef, useState } from 'react'; +import { useRef } from 'react'; import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria'; import { useDisclosureState } from 'react-stately'; +import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; import { Section, SectionBody, SectionHeader, SectionHeaderContent } from './StaticSection'; interface InteractiveSectionTab { @@ -34,6 +35,10 @@ export function InteractiveSection(props: { header?: React.ReactNode; /** Children to display within the container */ overlay?: React.ReactNode; + /** State key to use with a store */ + stateKey?: string; + /** Icon for the tabs select */ + selectIcon?: React.ReactNode; }) { const { id, @@ -45,12 +50,9 @@ export function InteractiveSection(props: { header, overlay, toggleIcon = '▶', + selectIcon, + stateKey = 'interactive-section', } = props; - - const [selectedTabKey, setSelectedTab] = useState(defaultTab); - const selectedTab: InteractiveSectionTab | undefined = - tabs.find((tab) => tab.key === selectedTabKey) ?? tabs[0]; - const state = useDisclosureState({ defaultExpanded: defaultOpened, }); @@ -59,6 +61,10 @@ export function InteractiveSection(props: { const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef); const { buttonProps } = useButton(triggerProps, triggerRef); const { isFocusVisible, focusProps } = useFocusRing(); + const store = useSelectState(stateKey, defaultTab); + + const selectedTab: InteractiveSectionTab | undefined = + tabs.find((tab) => tab.key === store.key) ?? tabs[0]; return (
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: we prevent default here */}
{tabs.length > 1 ? ( - + ) : null}
diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 1568a0744..8b67bbedc 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -3,15 +3,15 @@ import { OpenAPIMediaTypeExamplesBody, OpenAPIMediaTypeExamplesSelector, } from './OpenAPICodeSampleInteractive'; -import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; +import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector'; import { ScalarApiButton } from './ScalarApiButton'; -import { StaticSection } from './StaticSection'; import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples'; +import { type OpenAPIContext, getOpenAPIClientContext } from './context'; import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { stringifyOpenAPI } from './stringifyOpenAPI'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; +import type { OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; -import { checkIsReference, createStateKey } from './utils'; +import { checkIsReference } from './utils'; const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const; @@ -21,9 +21,9 @@ const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-code */ export function OpenAPICodeSample(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { - const { data } = props; + const { data, context } = props; // If code samples are disabled at operation level, we don't display the code samples. if (data.operation['x-codeSamples'] === false) { @@ -45,11 +45,12 @@ export function OpenAPICodeSample(props: { } return ( - - } className="openapi-codesample"> - - - + ); } @@ -58,7 +59,7 @@ export function OpenAPICodeSample(props: { */ function generateCodeSamples(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context } = props; @@ -153,6 +154,7 @@ function generateCodeSamples(props: { method={data.method} path={data.path} renderers={renderers} + blockKey={context.blockKey} /> ), footer: ( @@ -189,7 +191,7 @@ export interface MediaTypeRenderer { function OpenAPICodeSampleFooter(props: { data: OpenAPIOperationData; renderers: MediaTypeRenderer[]; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context, renderers } = props; const { method, path } = data; @@ -213,11 +215,20 @@ function OpenAPICodeSampleFooter(props: { method={data.method} path={data.path} renderers={renderers} + selectIcon={context.icons.chevronDown} + blockKey={context.blockKey} /> ) : ( )} - {!hideTryItPanel && } + {!hideTryItPanel && ( + + )} ); } @@ -227,7 +238,7 @@ function OpenAPICodeSampleFooter(props: { */ function getCustomCodeSamples(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContext; }) { const { data, context } = props; diff --git a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx index 17cf16a62..b1c40b101 100644 --- a/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx +++ b/packages/react-openapi/src/OpenAPICodeSampleInteractive.tsx @@ -1,86 +1,70 @@ 'use client'; import clsx from 'clsx'; -import { useCallback } from 'react'; -import { useStore } from 'zustand'; import type { MediaTypeRenderer } from './OpenAPICodeSample'; -import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState'; - -type MediaTypeState = { - mediaType: string; - setMediaType: (mediaType: string) => void; -}; - -function useMediaTypeState( - data: { method: string; path: string }, - defaultKey: string -): MediaTypeState { - const { method, path } = data; - const store = useStore(getOrCreateTabStoreByKey(`media-type-${method}-${path}`, defaultKey)); - if (typeof store.tabKey !== 'string') { - throw new Error('Media type key is not a string'); - } - return { - mediaType: store.tabKey, - setMediaType: useCallback((index: string) => store.setTabKey(index), [store.setTabKey]), - }; -} - -function useMediaTypeSampleIndexState(data: { method: string; path: string }, mediaType: string) { - const { method, path } = data; - const store = useStore( - getOrCreateTabStoreByKey(`media-type-sample-${mediaType}-${method}-${path}`, 0) - ); - if (typeof store.tabKey !== 'number') { - throw new Error('Example key is not a number'); - } - return { - index: store.tabKey, - setIndex: useCallback((index: number) => store.setTabKey(index), [store.setTabKey]), - }; -} +import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; +import { createStateKey } from './utils'; export function OpenAPIMediaTypeExamplesSelector(props: { method: string; path: string; renderers: MediaTypeRenderer[]; + selectIcon?: React.ReactNode; + blockKey?: string; }) { - const { method, path, renderers } = props; + const { method, path, renderers, selectIcon, blockKey } = props; if (!renderers[0]) { throw new Error('No renderers provided'); } - const state = useMediaTypeState({ method, path }, renderers[0].mediaType); - const selected = renderers.find((r) => r.mediaType === state.mediaType) || renderers[0]; + const stateKey = createStateKey('request-body-media-type', blockKey); + const state = useSelectState(stateKey, renderers[0].mediaType); + const selected = renderers.find((r) => r.mediaType === state.key) || renderers[0]; return (
- - + +
); } function MediaTypeSelector(props: { - state: MediaTypeState; + stateKey: string; renderers: MediaTypeRenderer[]; + selectIcon?: React.ReactNode; }) { - const { renderers, state } = props; + const { renderers, stateKey, selectIcon } = props; if (renderers.length < 2) { return null; } + const items = renderers.map((renderer) => ({ + key: renderer.mediaType, + label: renderer.mediaType, + })); + return ( - + ); } @@ -88,25 +72,31 @@ function ExamplesSelector(props: { method: string; path: string; renderer: MediaTypeRenderer; + selectIcon?: React.ReactNode; }) { - const { method, path, renderer } = props; - const state = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType); + const { method, path, renderer, selectIcon } = props; if (renderer.examples.length < 2) { return null; } + const items = renderer.examples.map((example, index) => ({ + key: index, + label: example.example.summary || `Example ${index + 1}`, + })); + return ( - + ); } @@ -114,14 +104,18 @@ export function OpenAPIMediaTypeExamplesBody(props: { method: string; path: string; renderers: MediaTypeRenderer[]; + blockKey?: string; }) { - const { renderers, method, path } = props; + const { renderers, method, path, blockKey } = props; if (!renderers[0]) { throw new Error('No renderers provided'); } - const mediaTypeState = useMediaTypeState({ method, path }, renderers[0].mediaType); - const selected = - renderers.find((r) => r.mediaType === mediaTypeState.mediaType) ?? renderers[0]; + + const mediaTypeState = useSelectState( + createStateKey('request-body-media-type', blockKey), + renderers[0].mediaType + ); + const selected = renderers.find((r) => r.mediaType === mediaTypeState.key) ?? renderers[0]; if (selected.examples.length === 0) { return selected.element; } @@ -130,10 +124,13 @@ export function OpenAPIMediaTypeExamplesBody(props: { function ExamplesBody(props: { method: string; path: string; renderer: MediaTypeRenderer }) { const { method, path, renderer } = props; - const exampleState = useMediaTypeSampleIndexState({ method, path }, renderer.mediaType); - const example = renderer.examples[exampleState.index] ?? renderer.examples[0]; + const exampleState = useSelectState( + `media-type-sample-${renderer.mediaType}-${method}-${path}`, + renderer.mediaType + ); + const example = renderer.examples[Number(exampleState.key)] ?? renderer.examples[0]; if (!example) { - throw new Error(`No example found for index ${exampleState.index}`); + throw new Error(`No example found for key ${exampleState.key}`); } return example.element; } diff --git a/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx new file mode 100644 index 000000000..d8b80a9f7 --- /dev/null +++ b/packages/react-openapi/src/OpenAPICodeSampleSelector.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useCallback } from 'react'; +import type { Key } from 'react-aria'; +import { useStore } from 'zustand'; +import { OpenAPIPath } from './OpenAPIPath'; +import { OpenAPISelect, OpenAPISelectItem } from './OpenAPISelect'; +import { StaticSection } from './StaticSection'; +import type { OpenAPIClientContext } from './context'; +import { getOrCreateStoreByKey } from './getOrCreateStoreByKey'; +import type { OpenAPIOperationData } from './types'; + +function useCodeSampleState(initialKey: Key = 'default') { + const store = useStore(getOrCreateStoreByKey('codesample', initialKey)); + return { + key: store.key, + setKey: useCallback((key: Key) => store.setKey(key), [store.setKey]), + }; +} + +type CodeSampleItem = OpenAPISelectItem & { + body: React.ReactNode; + footer?: React.ReactNode; +}; + +function OpenAPICodeSampleHeader(props: { + items: CodeSampleItem[]; + data: OpenAPIOperationData; + selectIcon?: React.ReactNode; + context: OpenAPIClientContext; +}) { + const { data, items, selectIcon, context } = props; + + return ( + <> + + {items.length > 1 ? ( + + {items.map((item) => ( + + {item.label} + + ))} + + ) : items[0] ? ( + {items[0].label} + ) : null} + + ); +} + +export function OpenAPICodeSampleBody(props: { + items: CodeSampleItem[]; + data: OpenAPIOperationData; + selectIcon?: React.ReactNode; + context: OpenAPIClientContext; +}) { + const { items, data, selectIcon, context } = props; + if (!items[0]) { + throw new Error('No items provided'); + } + + const state = useCodeSampleState(items[0]?.key); + + const selected = items.find((item) => item.key === state.key) || items[0]; + + if (!selected) { + return null; + } + + return ( + + } + className="openapi-codesample" + > +
+ {selected.body ? selected.body : null} + {selected.footer ? selected.footer : null} +
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPICopyButton.tsx b/packages/react-openapi/src/OpenAPICopyButton.tsx index 954ff9b6a..df8d258e9 100644 --- a/packages/react-openapi/src/OpenAPICopyButton.tsx +++ b/packages/react-openapi/src/OpenAPICopyButton.tsx @@ -2,11 +2,14 @@ import { useState } from 'react'; import { Button, type ButtonProps, Tooltip, TooltipTrigger } from 'react-aria-components'; +import type { OpenAPIClientContext } from './context'; +import { t } from './translate'; export function OpenAPICopyButton( props: ButtonProps & { value: string; children: React.ReactNode; + context: OpenAPIClientContext; label?: string; /** * Whether to show a tooltip. @@ -15,7 +18,7 @@ export function OpenAPICopyButton( withTooltip?: boolean; } ) { - const { value, label, children, onPress, className, withTooltip = true } = props; + const { value, label, children, onPress, className, context, withTooltip = true } = props; const [copied, setCopied] = useState(false); const [isOpen, setIsOpen] = useState(false); @@ -60,7 +63,9 @@ export function OpenAPICopyButton( offset={4} className="openapi-tooltip" > - {copied ? 'Copied' : label || 'Copy to clipboard'} + {copied + ? t(context.translation, 'copied') + : label || t(context.translation, 'copy_to_clipboard')} ); diff --git a/packages/react-openapi/src/OpenAPIDisclosure.tsx b/packages/react-openapi/src/OpenAPIDisclosure.tsx index 09babaf98..56e663b44 100644 --- a/packages/react-openapi/src/OpenAPIDisclosure.tsx +++ b/packages/react-openapi/src/OpenAPIDisclosure.tsx @@ -1,41 +1,43 @@ 'use client'; +import clsx from 'clsx'; +import type React from 'react'; import { useState } from 'react'; -import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components'; -import type { OpenAPIClientContext } from './types'; +import { Button, Disclosure, DisclosurePanel } from 'react-aria-components'; /** * Display an interactive OpenAPI disclosure. */ export function OpenAPIDisclosure(props: { - context: OpenAPIClientContext; + icon: React.ReactNode; + header: React.ReactNode; children: React.ReactNode; - label: string; + label: string | ((isExpanded: boolean) => string); + className?: string; }): React.JSX.Element { - const { context, children, label } = props; + const { icon, header, label, children, className } = props; const [isExpanded, setIsExpanded] = useState(false); return ( - - - + {isExpanded ? children : null} diff --git a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx index 82c89c28a..ecef441b7 100644 --- a/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx +++ b/packages/react-openapi/src/OpenAPIDisclosureGroup.tsx @@ -1,6 +1,6 @@ 'use client'; -import { createContext, useContext, useRef, useState } from 'react'; +import { createContext, useContext, useRef } from 'react'; import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria'; import { type DisclosureGroupProps, @@ -8,18 +8,23 @@ import { useDisclosureGroupState, useDisclosureState, } from 'react-stately'; +import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; interface Props { groups: TDisclosureGroup[]; icon?: React.ReactNode; + /** State key to use with a store */ + selectStateKey?: string; + /** Icon to display for the select */ + selectIcon?: React.ReactNode; } type TDisclosureGroup = { - id: string; + key: string; label: string | React.ReactNode; tabs?: { - id: string; - label?: string | React.ReactNode; + key: string; + label: string | React.ReactNode; body?: React.ReactNode; }[]; }; @@ -30,24 +35,35 @@ const DisclosureGroupStateContext = createContext(n * Display an interactive OpenAPI disclosure group. */ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) { - const { icon, groups } = props; + const { icon, groups, selectStateKey, selectIcon } = props; const state = useDisclosureGroupState(props); return ( {groups.map((group) => ( - + ))} ); } -function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) { - const { icon, group } = props; +function DisclosureItem(props: { + group: TDisclosureGroup; + icon?: React.ReactNode; + selectStateKey?: string; + selectIcon?: React.ReactNode; +}) { + const { icon, group, selectStateKey, selectIcon } = props; const defaultId = useId(); - const id = group.id || defaultId; + const id = group.key || defaultId; const groupState = useContext(DisclosureGroupStateContext); const isExpanded = groupState?.expandedKeys.has(id) || false; const state = useDisclosureState({ @@ -60,7 +76,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }); const panelRef = useRef(null); - const triggerRef = useRef(null); + const triggerRef = useRef(null); const isDisabled = groupState?.isDisabled || !group.tabs?.length || false; const { buttonProps: triggerProps, panelProps } = useDisclosure( { @@ -74,58 +90,62 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode const { buttonProps } = useButton(triggerProps, triggerRef); const { isFocusVisible, focusProps } = useFocusRing(); - const defaultTab = group.tabs?.[0]?.id || ''; - const [selectedTabKey, setSelectedTabKey] = useState(defaultTab); - const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey); + const defaultTab = group.tabs?.[0]?.key || ''; + const store = useSelectState(selectStateKey, defaultTab); + const selectedTab = group.tabs?.find((tab) => tab.key === store.key) || group.tabs?.[0]; return (
-
- - {group.tabs ? ( -
- {group.tabs?.length > 1 ? ( - - ) : group.tabs[0]?.label ? ( - {group.tabs[0].label} - ) : null} -
- ) : null} + + {group.tabs ? ( +
e.stopPropagation()} + > + {group.tabs?.length > 1 ? ( + { + state.expand(); + }} + items={group.tabs} + placement="bottom end" + > + {group.tabs.map((tab) => ( + + {tab.label} + + ))} + + ) : group.tabs[0]?.label ? ( + {group.tabs[0].label} + ) : null} +
+ ) : null} +
{state.isExpanded && selectedTab && ( diff --git a/packages/react-openapi/src/OpenAPIExample.tsx b/packages/react-openapi/src/OpenAPIExample.tsx new file mode 100644 index 000000000..7f9328d6f --- /dev/null +++ b/packages/react-openapi/src/OpenAPIExample.tsx @@ -0,0 +1,55 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import type { OpenAPIContext, OpenAPIUniversalContext } from './context'; +import { json2xml } from './json2xml'; +import { stringifyOpenAPI } from './stringifyOpenAPI'; +import { t } from './translate'; + +/** + * Display an example. + */ +export function OpenAPIExample(props: { + example: OpenAPIV3.ExampleObject; + context: OpenAPIContext; + syntax: string; +}) { + const { example, context, syntax } = props; + const code = stringifyExample({ example, xml: syntax === 'xml' }); + + if (code === null) { + return ; + } + + return context.renderCodeBlock({ code, syntax }); +} + +function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null { + const { example, xml } = args; + + if (!example.value) { + return null; + } + + if (typeof example.value === 'string') { + return example.value; + } + + if (xml) { + return json2xml(example.value); + } + + return stringifyOpenAPI(example.value, null, 2); +} + +/** + * Empty response example. + */ +export function OpenAPIEmptyExample(props: { + context: OpenAPIUniversalContext; +}) { + const { context } = props; + return ( +
+            

{t(context.translation, 'no_content')}

+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIMediaType.tsx b/packages/react-openapi/src/OpenAPIMediaType.tsx new file mode 100644 index 000000000..e929319f2 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIMediaType.tsx @@ -0,0 +1,139 @@ +'use client'; + +import type { Key } from 'react-aria'; +import { OpenAPIEmptyExample } from './OpenAPIExample'; +import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; +import { StaticSection } from './StaticSection'; +import type { OpenAPIClientContext } from './context'; + +type OpenAPIMediaTypeItem = OpenAPISelectItem & { + body: React.ReactNode; + examples?: OpenAPIMediaTypeItem[]; +}; + +/** + * Get the state of the response examples select. + */ +export function useMediaTypesState(stateKey: string | undefined, initialKey: Key = 'default') { + return useSelectState(stateKey, initialKey); +} + +function useMediaTypeExamplesState(stateKey: string | undefined, initialKey: Key = 'default') { + return useSelectState(stateKey, initialKey); +} + +export function OpenAPIMediaTypeContent(props: { + items: OpenAPIMediaTypeItem[]; + selectIcon?: React.ReactNode; + stateKey: string; + context: OpenAPIClientContext; +}) { + const { stateKey, items, selectIcon, context } = props; + const state = useMediaTypesState(stateKey, items[0]?.key); + + const examples = items.find((item) => item.key === state.key)?.examples ?? []; + + if (!items.length && !examples.length) { + return null; + } + + return ( + 1 || examples.length > 1 ? ( + + ) : null + } + className="openapi-response-media-types-examples" + > + + + ); +} + +function OpenAPIMediaTypeFooter(props: { + items: OpenAPIMediaTypeItem[]; + examples?: OpenAPIMediaTypeItem[]; + selectIcon?: React.ReactNode; + stateKey: string; +}) { + const { items, examples, stateKey, selectIcon } = props; + + return ( + <> + {items.length > 1 && ( + + {items.map((item) => ( + + {item.label} + + ))} + + )} + + {examples && examples.length > 1 ? ( + + {examples.map((example) => ( + + {example.label} + + ))} + + ) : null} + + ); +} + +function OpenAPIMediaTypeBody(props: { + items: OpenAPIMediaTypeItem[]; + examples?: OpenAPIMediaTypeItem[]; + stateKey: string; + context: OpenAPIClientContext; +}) { + const { stateKey, items, examples, context } = props; + const state = useMediaTypesState(stateKey, items[0]?.key); + + const selectedItem = items.find((item) => item.key === state.key) ?? items[0]; + + const exampleState = useMediaTypeExamplesState( + `${stateKey}-examples`, + selectedItem?.examples?.[0]?.key + ); + + if (!selectedItem) { + return null; + } + + if (examples) { + const selectedExample = + examples.find((example) => example.key === exampleState.key) ?? examples[0]; + + if (!selectedExample) { + return ; + } + + return selectedExample.body; + } + + return selectedItem.body; +} diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index e5672c631..856ef5770 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -1,17 +1,10 @@ import clsx from 'clsx'; - -import type { - OpenAPICustomOperationProperties, - OpenAPIStability, - OpenAPIV3, -} from '@gitbook/openapi-parser'; -import { Markdown } from './Markdown'; import { OpenAPICodeSample } from './OpenAPICodeSample'; -import { OpenAPIPath } from './OpenAPIPath'; import { OpenAPIResponseExample } from './OpenAPIResponseExample'; -import { OpenAPISpec } from './OpenAPISpec'; -import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types'; -import { resolveDescription } from './utils'; +import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec'; +import { OpenAPISummary } from './common/OpenAPISummary'; +import { type OpenAPIContextInput, resolveOpenAPIContext } from './context'; +import type { OpenAPIOperationData } from './types'; /** * Display an interactive OpenAPI operation. @@ -19,110 +12,24 @@ import { resolveDescription } from './utils'; export function OpenAPIOperation(props: { className?: string; data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIContextInput; }) { - const { className, data, context } = props; - const { operation } = data; + const { className, data, context: contextInput } = props; - const clientContext: OpenAPIClientContext = { - defaultInteractiveOpened: context.defaultInteractiveOpened, - icons: context.icons, - blockKey: context.blockKey, - }; + const context = resolveOpenAPIContext(contextInput); return (
-
- {(operation.deprecated || operation['x-stability']) && ( -
- {operation.deprecated && ( -
Deprecated
- )} - {operation['x-stability'] && ( - - )} -
- )} - {operation.summary - ? context.renderHeading({ - deprecated: operation.deprecated ?? false, - stability: operation['x-stability'], - title: operation.summary, - }) - : null} - -
+
-
- {operation['x-deprecated-sunset'] ? ( -
- This operation is deprecated and will be sunset on{' '} - - {operation['x-deprecated-sunset']} - - {'.'} -
- ) : null} - - -
+
- - + +
); } - -function OpenAPIOperationDescription(props: { - operation: OpenAPIV3.OperationObject; - context: OpenAPIContextProps; -}) { - const { operation } = props; - if (operation['x-gitbook-description-document']) { - return ( -
- {props.context.renderDocument({ - document: operation['x-gitbook-description-document'], - })} -
- ); - } - - const description = resolveDescription(operation); - if (!description) { - return null; - } - - return ( -
- -
- ); -} - -const stabilityEnum = { - experimental: 'Experimental', - alpha: 'Alpha', - beta: 'Beta', - stable: 'Stable', -} as const; - -function OpenAPIOperationStability(props: { stability: OpenAPIStability }) { - const { stability } = props; - - const foundStability = stabilityEnum[stability]; - - if (!foundStability) { - return null; - } - - return ( -
- {foundStability} -
- ); -} diff --git a/packages/react-openapi/src/OpenAPIOperationDescription.tsx b/packages/react-openapi/src/OpenAPIOperationDescription.tsx new file mode 100644 index 000000000..c0a2b248b --- /dev/null +++ b/packages/react-openapi/src/OpenAPIOperationDescription.tsx @@ -0,0 +1,34 @@ +import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; +import { Markdown } from './Markdown'; +import type { OpenAPIContext } from './context'; +import { resolveDescription } from './utils'; + +/** + * Display the description of an OpenAPI operation. + */ +export function OpenAPIOperationDescription(props: { + operation: OpenAPIV3.OperationObject; + context: OpenAPIContext; +}) { + const { operation } = props; + if (operation['x-gitbook-description-document']) { + return ( +
+ {props.context.renderDocument({ + document: operation['x-gitbook-description-document'], + })} +
+ ); + } + + const description = resolveDescription(operation); + if (!description) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIOperationStability.tsx b/packages/react-openapi/src/OpenAPIOperationStability.tsx new file mode 100644 index 000000000..a60f59c93 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIOperationStability.tsx @@ -0,0 +1,39 @@ +import type { OpenAPIStability } from '@gitbook/openapi-parser'; +import type { OpenAPIContext } from './context'; +import { t } from './translate'; + +/** + * Display the stability of an OpenAPI operation. + */ +export function OpenAPIOperationStability(props: { + stability: OpenAPIStability; + context: OpenAPIContext; +}) { + const { stability, context } = props; + + const stabilityLabel = getStabilityLabel(stability, context); + + if (!stabilityLabel) { + return null; + } + + return ( +
{stabilityLabel}
+ ); +} + +/** + * Get the stability label for the given stability level. + */ +function getStabilityLabel(stability: OpenAPIStability, context: OpenAPIContext) { + switch (stability) { + case 'experimental': + return t(context.translation, 'stability_experimental'); + case 'alpha': + return t(context.translation, 'stability_alpha'); + case 'beta': + return t(context.translation, 'stability_beta'); + default: + return null; + } +} diff --git a/packages/react-openapi/src/OpenAPIPath.tsx b/packages/react-openapi/src/OpenAPIPath.tsx index 45be7c38d..90a1d66cd 100644 --- a/packages/react-openapi/src/OpenAPIPath.tsx +++ b/packages/react-openapi/src/OpenAPIPath.tsx @@ -1,5 +1,6 @@ import { OpenAPICopyButton } from './OpenAPICopyButton'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; +import { type OpenAPIUniversalContext, getOpenAPIClientContext } from './context'; +import type { OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; /** @@ -7,25 +8,44 @@ import { getDefaultServerURL } from './util/server'; */ export function OpenAPIPath(props: { data: OpenAPIOperationData; - context: OpenAPIContextProps; + context: OpenAPIUniversalContext; + /** Whether to show the server URL. + * @default true + */ + withServer?: boolean; + /** + * Whether the path is copyable. + * @default true + */ + canCopy?: boolean; }) { - const { data } = props; + const { data, context, withServer = true, canCopy = true } = props; const { method, path, operation } = data; const server = getDefaultServerURL(data.servers); const formattedPath = formatPath(path); + const element = (() => { + return ( + <> + {withServer ? {server} : null} + {formattedPath} + + ); + })(); + return (
{method}
- {server} - {formattedPath} + {element}
); diff --git a/packages/react-openapi/src/OpenAPIRequestBody.tsx b/packages/react-openapi/src/OpenAPIRequestBody.tsx index c7c2515e4..0316e85b0 100644 --- a/packages/react-openapi/src/OpenAPIRequestBody.tsx +++ b/packages/react-openapi/src/OpenAPIRequestBody.tsx @@ -1,8 +1,10 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { InteractiveSection } from './InteractiveSection'; import { OpenAPIRootSchema } from './OpenAPISchemaServer'; -import type { OpenAPIClientContext } from './types'; -import { checkIsReference } from './utils'; +import type { OpenAPIClientContext } from './context'; +import { t } from './translate'; +import type { OpenAPIOperationData, OpenAPIWebhookData } from './types'; +import { checkIsReference, createStateKey } from './utils'; /** * Display an interactive request body. @@ -10,8 +12,9 @@ import { checkIsReference } from './utils'; export function OpenAPIRequestBody(props: { requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject; context: OpenAPIClientContext; + data: OpenAPIOperationData | OpenAPIWebhookData; }) { - const { requestBody, context } = props; + const { requestBody, context, data } = props; if (checkIsReference(requestBody)) { return null; @@ -19,8 +22,10 @@ export function OpenAPIRequestBody(props: { return ( { return { diff --git a/packages/react-openapi/src/OpenAPIResponse.tsx b/packages/react-openapi/src/OpenAPIResponse.tsx index 163d3eb8e..3c005ec29 100644 --- a/packages/react-openapi/src/OpenAPIResponse.tsx +++ b/packages/react-openapi/src/OpenAPIResponse.tsx @@ -1,7 +1,9 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; +import { OpenAPISchemaPresentation } from './OpenAPISchema'; import { OpenAPISchemaProperties } from './OpenAPISchemaServer'; -import type { OpenAPIClientContext } from './types'; +import type { OpenAPIClientContext } from './context'; +import { tString } from './translate'; import { parameterToProperty, resolveDescription } from './utils'; /** @@ -27,7 +29,31 @@ export function OpenAPIResponse(props: { return (
{headers.length > 0 ? ( - + + } + icon={context.icons.plus} + label={(isExpanded) => + tString( + context.translation, + isExpanded ? 'hide' : 'show', + tString( + context.translation, + headers.length === 1 ? 'header' : 'headers' + ) + ) + } + > parameterToProperty({ name, ...header }) @@ -36,13 +62,21 @@ export function OpenAPIResponse(props: { /> ) : null} -
- -
+ {mediaType.schema && ( +
+ +
+ )}
); } diff --git a/packages/react-openapi/src/OpenAPIResponseExample.tsx b/packages/react-openapi/src/OpenAPIResponseExample.tsx index fe7f2666e..b4b865f50 100644 --- a/packages/react-openapi/src/OpenAPIResponseExample.tsx +++ b/packages/react-openapi/src/OpenAPIResponseExample.tsx @@ -1,19 +1,20 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { Markdown } from './Markdown'; -import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs'; -import { StaticSection } from './StaticSection'; -import { generateSchemaExample } from './generateSchemaExample'; -import { json2xml } from './json2xml'; -import { stringifyOpenAPI } from './stringifyOpenAPI'; -import type { OpenAPIContextProps, OpenAPIOperationData } from './types'; -import { checkIsReference, createStateKey, resolveDescription } from './utils'; +import { OpenAPIEmptyExample, OpenAPIExample } from './OpenAPIExample'; +import { OpenAPIMediaTypeContent } from './OpenAPIMediaType'; +import { OpenAPIResponseExampleContent } from './OpenAPIResponseExampleContent'; +import { type OpenAPIContext, getOpenAPIClientContext } from './context'; +import type { OpenAPIOperationData, OpenAPIWebhookData } from './types'; +import { getExampleFromReference, getExamples } from './util/example'; +import { createStateKey, getStatusCodeDefaultLabel, resolveDescription } from './utils'; +import { checkIsReference } from './utils'; /** * Display an example of the response content. */ export function OpenAPIResponseExample(props: { - data: OpenAPIOperationData; - context: OpenAPIContextProps; + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIContext; }) { const { data, context } = props; @@ -40,56 +41,63 @@ export function OpenAPIResponseExample(props: { return Number(a) - Number(b); }); - const tabs = responses.map(([key, responseObject]) => { - const description = resolveDescription(responseObject); + const tabs = responses + .filter(([_, responseObject]) => responseObject && typeof responseObject === 'object') + .map(([key, responseObject]) => { + const description = resolveDescription(responseObject); + const label = description ? ( + + ) : ( + getStatusCodeDefaultLabel(key, context) + ); + + if (checkIsReference(responseObject)) { + return { + key: key, + label, + statusCode: key, + body: ( + + ), + }; + } + + if (!responseObject.content || Object.keys(responseObject.content).length === 0) { + return { + key: key, + label, + statusCode: key, + body: , + }; + } - if (checkIsReference(responseObject)) { return { key: key, - label: key, - body: ( - - ), - footer: description ? : undefined, + label, + statusCode: key, + body: , }; - } - - if (!responseObject.content || Object.keys(responseObject.content).length === 0) { - return { - key: key, - label: key, - body: , - footer: description ? : undefined, - }; - } - - return { - key: key, - label: key, - body: , - footer: description ? : undefined, - }; - }); + }); if (tabs.length === 0) { return null; } return ( - - } className="openapi-response-example"> - - - + ); } function OpenAPIResponse(props: { - context: OpenAPIContextProps; + context: OpenAPIContext; content: { [media: string]: OpenAPIV3.MediaTypeObject; }; @@ -103,201 +111,26 @@ function OpenAPIResponse(props: { throw new Error('One media type is required'); } - if (entries.length === 1) { - const [mediaType, mediaTypeObject] = firstEntry; - return ( - - ); - } - const tabs = entries.map((entry) => { const [mediaType, mediaTypeObject] = entry; return { key: mediaType, label: mediaType, - body: ( - - ), - }; - }); - - return ( - - } className="openapi-response-media-types"> - - - - ); -} - -function OpenAPIResponseMediaType(props: { - mediaTypeObject: OpenAPIV3.MediaTypeObject; - mediaType: string; - context: OpenAPIContextProps; -}) { - const { mediaTypeObject, mediaType } = props; - const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType }); - const syntax = getSyntaxFromMediaType(mediaType); - const firstExample = examples[0]; - - if (!firstExample) { - return ; - } - - if (examples.length === 1) { - return ( - - ); - } - - const tabs = examples.map((example) => { - return { - key: example.key, - label: example.example.summary || example.key, - body: ( - - ), + body: <>, + examples: getExamples({ + mediaTypeObject, + mediaType, + context, + }), }; }); return ( - - } - className="openapi-response-media-type-examples" - > - - - - ); -} - -/** - * Display an example. - */ -function OpenAPIExample(props: { - example: OpenAPIV3.ExampleObject; - context: OpenAPIContextProps; - syntax: string; -}) { - const { example, context, syntax } = props; - const code = stringifyExample({ example, xml: syntax === 'xml' }); - - if (code === null) { - return ; - } - - return context.renderCodeBlock({ code, syntax }); -} - -function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null { - const { example, xml } = args; - - if (!example.value) { - return null; - } - - if (typeof example.value === 'string') { - return example.value; - } - - if (xml) { - return json2xml(example.value); - } - - return stringifyOpenAPI(example.value, null, 2); -} - -/** - * Get the syntax from a media type. - */ -function getSyntaxFromMediaType(mediaType: string): string { - if (mediaType.includes('json')) { - return 'json'; - } - - if (mediaType === 'application/xml') { - return 'xml'; - } - - return 'text'; -} - -/** - * Get examples from a media type object. - */ -function getExamplesFromMediaTypeObject(args: { - mediaType: string; - mediaTypeObject: OpenAPIV3.MediaTypeObject; -}): { key: string; example: OpenAPIV3.ExampleObject }[] { - const { mediaTypeObject, mediaType } = args; - if (mediaTypeObject.examples) { - return Object.entries(mediaTypeObject.examples).map(([key, example]) => { - return { - key, - example: checkIsReference(example) ? getExampleFromReference(example) : example, - }; - }); - } - - if (mediaTypeObject.example) { - return [{ key: 'default', example: { value: mediaTypeObject.example } }]; - } - - if (mediaTypeObject.schema) { - if (mediaType === 'application/xml') { - // @TODO normally we should use the name of the schema but we don't have it - // fix it when we got the reference name - const root = mediaTypeObject.schema.xml?.name ?? 'object'; - return [ - { - key: 'default', - example: { - value: { - [root]: generateSchemaExample(mediaTypeObject.schema, { - xml: mediaType === 'application/xml', - }), - }, - }, - }, - ]; - } - return [ - { - key: 'default', - example: { value: generateSchemaExample(mediaTypeObject.schema) }, - }, - ]; - } - return []; -} - -/** - * Empty response example. - */ -function OpenAPIEmptyResponseExample() { - return ( -
-            

No body

-
+ ); } - -/** - * Generate an example from a reference object. - */ -function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject { - return { summary: 'Unresolved reference', value: { $ref: ref.$ref } }; -} diff --git a/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx new file mode 100644 index 000000000..0b193ed3f --- /dev/null +++ b/packages/react-openapi/src/OpenAPIResponseExampleContent.tsx @@ -0,0 +1,123 @@ +'use client'; + +import clsx from 'clsx'; +import type { Key } from 'react-aria'; +import { OpenAPISelect, OpenAPISelectItem, useSelectState } from './OpenAPISelect'; +import { StaticSection } from './StaticSection'; +import { createStateKey, getStatusCodeClassName } from './utils'; + +type OpenAPIResponseExampleItem = OpenAPISelectItem & { + statusCode: string; + body: React.ReactNode; +}; + +/** + * Get the state of the response examples select. + */ +export function useResponseExamplesState( + blockKey: string | undefined, + initialKey: Key = 'default' +) { + return useSelectState(getResponseExampleStateKey(blockKey), initialKey); +} + +export function OpenAPIResponseExampleContent(props: { + items: OpenAPIResponseExampleItem[]; + blockKey?: string; + selectIcon?: React.ReactNode; +}) { + const { blockKey, items, selectIcon } = props; + + return ( + + } + className="openapi-response-examples" + > + + + ); +} + +function OpenAPIResponseExampleHeader(props: { + items: OpenAPIResponseExampleItem[]; + blockKey?: string; + selectIcon?: React.ReactNode; +}) { + const { items, blockKey, selectIcon } = props; + + if (items.length === 1) { + const item = items[0]; + + if (!item) { + return null; + } + + return ( + + + {item.statusCode} + + {item.label} + + ); + } + + return ( + + {items.map((item) => ( + + + {item.statusCode} + + {item.label} + + ))} + + ); +} + +function OpenAPIResponseExampleBody(props: { + items: OpenAPIResponseExampleItem[]; + blockKey?: string; +}) { + const { blockKey, items } = props; + const state = useResponseExamplesState(blockKey, items[0]?.key); + + const selectedItem = items.find((item) => item.key === state.key) ?? items[0]; + + if (!selectedItem) { + return null; + } + + return
{selectedItem.body}
; +} + +/** + * Return the state key for the response examples. + */ +function getResponseExampleStateKey(blockKey: string | undefined) { + return createStateKey('openapi-responses', blockKey); +} diff --git a/packages/react-openapi/src/OpenAPIResponses.tsx b/packages/react-openapi/src/OpenAPIResponses.tsx index 4e06a781a..c4565aa78 100644 --- a/packages/react-openapi/src/OpenAPIResponses.tsx +++ b/packages/react-openapi/src/OpenAPIResponses.tsx @@ -1,9 +1,15 @@ +'use client'; + import type { OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'; +import clsx from 'clsx'; import { Markdown } from './Markdown'; import { OpenAPIDisclosureGroup } from './OpenAPIDisclosureGroup'; import { OpenAPIResponse } from './OpenAPIResponse'; +import { useResponseExamplesState } from './OpenAPIResponseExampleContent'; import { StaticSection } from './StaticSection'; -import type { OpenAPIClientContext } from './types'; +import type { OpenAPIClientContext } from './context'; +import { t } from './translate'; +import { createStateKey, getStatusCodeClassName, getStatusCodeDefaultLabel } from './utils'; /** * Display an interactive response body. @@ -14,45 +20,86 @@ export function OpenAPIResponses(props: { }) { const { responses, context } = props; + const groups = Object.entries(responses) + .filter(([_, response]) => response && typeof response === 'object') + .map(([statusCode, response]: [string, OpenAPIV3.ResponseObject]) => { + const tabs = (() => { + // If there is no content, but there are headers, we need to show the headers + if ( + (!response.content || !Object.keys(response.content).length) && + response.headers && + Object.keys(response.headers).length + ) { + return [ + { + key: 'default', + label: '', + body: ( + + ), + }, + ]; + } + + return Object.entries(response.content ?? {}).map(([contentType, mediaType]) => ({ + key: contentType, + label: contentType, + body: ( + + ), + })); + })(); + + const description = response.description; + + return { + key: statusCode, + label: ( +
+ + {statusCode} + + {description ? ( + + ) : ( + getStatusCodeDefaultLabel(statusCode, context) + )} +
+ ), + tabs, + }; + }); + + const state = useResponseExamplesState(context.blockKey, groups[0]?.key); + return ( - + { - const content = Object.entries(response.content ?? {}); - const description = response.description; - - return { - id: statusCode, - label: ( -
- - {statusCode} - - {description ? ( - - ) : null} -
- ), - tabs: content.map(([contentType, mediaType]) => ({ - id: contentType, - label: contentType, - body: ( - - ), - })), - }; - } - )} + expandedKeys={state.key ? new Set([state.key]) : new Set()} + onExpandedChange={(keys) => { + const key = keys.values().next().value ?? null; + state.setKey(key); + }} + groups={groups} + selectIcon={context.icons.chevronDown} + selectStateKey={createStateKey('response-media-types', context.blockKey)} />
); diff --git a/packages/react-openapi/src/OpenAPISchema.test.ts b/packages/react-openapi/src/OpenAPISchema.test.ts index e3fdc754c..4d2adcfc0 100644 --- a/packages/react-openapi/src/OpenAPISchema.test.ts +++ b/packages/react-openapi/src/OpenAPISchema.test.ts @@ -35,6 +35,86 @@ describe('getSchemaAlternatives', () => { ]); }); + it('merges string enum', () => { + expect( + getSchemaAlternatives({ + oneOf: [ + { + oneOf: [ + { + type: 'string', + enum: ['a', 'b'], + }, + { + type: 'string', + enum: ['c', 'd'], + nullable: true, + }, + ], + }, + ], + }) + ).toEqual([ + { + type: 'string', + enum: ['a', 'b', 'c', 'd'], + nullable: true, + }, + ]); + }); + + it('merges objects with allOf', () => { + expect( + getSchemaAlternatives({ + allOf: [ + { + type: 'object', + properties: { + name: { + type: 'string', + }, + map: { + type: 'string', + }, + description: { + type: 'string', + }, + }, + required: ['name'], + }, + { + type: 'object', + properties: { + externalId: { + type: 'string', + }, + }, + required: ['map', 'externalId'], + }, + ], + }) + ).toEqual([ + { + type: 'object', + properties: { + name: { + type: 'string', + }, + map: { + type: 'string', + }, + description: { + type: 'string', + }, + externalId: { + type: 'string', + }, + }, + required: ['name', 'map', 'externalId'], + }, + ]); + }); + it('should not flatten oneOf and allOf', () => { expect( getSchemaAlternatives({ diff --git a/packages/react-openapi/src/OpenAPISchema.tsx b/packages/react-openapi/src/OpenAPISchema.tsx index 3e41f6501..e9f8c6707 100644 --- a/packages/react-openapi/src/OpenAPISchema.tsx +++ b/packages/react-openapi/src/OpenAPISchema.tsx @@ -4,81 +4,114 @@ import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; import { useId } from 'react'; +import type { ComponentPropsWithoutRef } from 'react'; import clsx from 'clsx'; import { Markdown } from './Markdown'; import { OpenAPICopyButton } from './OpenAPICopyButton'; import { OpenAPIDisclosure } from './OpenAPIDisclosure'; import { OpenAPISchemaName } from './OpenAPISchemaName'; +import type { OpenAPIClientContext } from './context'; import { retrocycle } from './decycle'; -import type { OpenAPIClientContext } from './types'; +import { getDisclosureLabel } from './getDisclosureLabel'; +import { stringifyOpenAPI } from './stringifyOpenAPI'; +import { tString } from './translate'; import { checkIsReference, resolveDescription, resolveFirstExample } from './utils'; type CircularRefsIds = Map; export interface OpenAPISchemaPropertyEntry { - propertyName?: string | undefined; - required?: boolean | undefined; + propertyName?: string; + required?: boolean | null; schema: OpenAPIV3.SchemaObject; } /** * Render a property of an OpenAPI schema. */ -function OpenAPISchemaProperty(props: { - property: OpenAPISchemaPropertyEntry; - context: OpenAPIClientContext; - circularRefs: CircularRefsIds; - className?: string; -}) { - const { circularRefs: parentCircularRefs, context, className, property } = props; +function OpenAPISchemaProperty( + props: { + property: OpenAPISchemaPropertyEntry; + context: OpenAPIClientContext; + circularRefs: CircularRefsIds; + className?: string; + } & Omit, 'property' | 'context' | 'circularRefs' | 'className'> +) { + const { circularRefs: parentCircularRefs, context, className, property, ...rest } = props; const { schema } = property; const id = useId(); - return ( -
- - {(() => { - const circularRefId = parentCircularRefs.get(schema); - // Avoid recursing infinitely, and instead render a link to the parent schema - if (circularRefId) { - return ; - } + const circularRefId = parentCircularRefs.get(schema); + // Avoid recursing infinitely, and instead render a link to the parent schema + if (circularRefId) { + return ; + } - const circularRefs = new Map(parentCircularRefs); - circularRefs.set(schema, id); + const circularRefs = new Map(parentCircularRefs); + circularRefs.set(schema, id); - const properties = getSchemaProperties(schema); - if (properties?.length) { - return ( - - ; + const content = (() => { + if (properties?.length) { + return ( + + ); + } + + if (alternatives) { + return ( +
+ {alternatives.map((alternativeSchema, index) => ( +
+ - - ); - } + {index < alternatives.length - 1 ? ( + + ) : null} +
+ ))} +
+ ); + } - const ancestors = new Set(circularRefs.keys()); - const alternatives = getSchemaAlternatives(schema, ancestors); - - if (alternatives) { - return alternatives.map((schema, index) => ( - - )); - } + return null; + })(); + + if (properties?.length) { + return ( + getDisclosureLabel({ schema, isExpanded, context })} + {...rest} + > + {content} + + ); + } - return null; - })()} + return ( +
+ {header} + {content}
); } @@ -108,6 +141,7 @@ function OpenAPISchemaProperties(props: { circularRefs={circularRefs} property={property} context={context} + style={{ animationDelay: `${index * 0.02}s` }} /> ); })} @@ -145,17 +179,23 @@ function OpenAPIRootSchema(props: { const id = useId(); const properties = getSchemaProperties(schema); + const description = resolveDescription(schema); if (properties?.length) { const circularRefs = new Map(parentCircularRefs); circularRefs.set(schema, id); return ( - + <> + {description ? ( + + ) : null} + + ); } @@ -192,31 +232,48 @@ function OpenAPISchemaAlternative(props: { context: OpenAPIClientContext; }) { const { schema, circularRefs, context } = props; - - const description = resolveDescription(schema); const properties = getSchemaProperties(schema); + return properties?.length ? ( + } + label={(isExpanded) => getDisclosureLabel({ schema, isExpanded, context })} + > + + + ) : ( + + ); +} + +function OpenAPISchemaAlternativeSeparator(props: { + schema: OpenAPIV3.SchemaObject; + context: OpenAPIClientContext; +}) { + const { schema, context } = props; + + const anyOf = schema.anyOf || schema.items?.anyOf; + const oneOf = schema.oneOf || schema.items?.oneOf; + const allOf = schema.allOf || schema.items?.allOf; + + if (!anyOf && !oneOf && !allOf) { + return null; + } + return ( - <> - {description ? ( - - ) : null} - - {properties?.length ? ( - - ) : ( - - )} - - + + {(anyOf || oneOf) && tString(context.translation, 'or')} + {allOf && tString(context.translation, 'and')} + ); } @@ -239,8 +296,9 @@ function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaO */ function OpenAPISchemaEnum(props: { schema: OpenAPIV3.SchemaObject & OpenAPICustomOperationProperties; + context: OpenAPIClientContext; }) { - const { schema } = props; + const { schema, context } = props; const enumValues = (() => { // Render x-gitbook-enum first, as it has a different format @@ -275,31 +333,34 @@ function OpenAPISchemaEnum(props: { } return ( -
- Available options: -
- {enumValues.map((item, index) => ( - - - {`${item.value}`} - - - ))} -
-
+ + {tString(context.translation, 'possible_values')}:{' '} + {enumValues.map((item, index) => ( + + + {`${item.value}`} + + + ))} + ); } /** * Render the top row of a schema. e.g: name, type, and required status. */ -function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry }) { +export function OpenAPISchemaPresentation(props: { + property: OpenAPISchemaPropertyEntry; + context: OpenAPIClientContext; +}) { const { property: { schema, propertyName, required }, + context, } = props; const description = resolveDescription(schema); @@ -312,6 +373,7 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry type={getSchemaTitle(schema)} propertyName={propertyName} required={required} + context={context} /> {typeof schema['x-deprecated-sunset'] === 'string' ? (
@@ -324,17 +386,27 @@ function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry {description ? ( ) : null} + {schema.default !== undefined ? ( + + Default:{' '} + + {typeof schema.default === 'string' && schema.default + ? schema.default + : stringifyOpenAPI(schema.default)} + + + ) : null} {typeof example === 'string' ? ( -
+ Example: {example} -
+ ) : null} {schema.pattern ? ( -
+ Pattern: {schema.pattern} -
+ ) : null} - +
); } @@ -406,6 +478,14 @@ export function getSchemaAlternatives( schema: OpenAPIV3.SchemaObject, ancestors: Set = new Set() ): OpenAPIV3.SchemaObject[] | null { + // Search for alternatives in the items property if it exists + if ( + schema.items && + ('oneOf' in schema.items || 'allOf' in schema.items || 'anyOf' in schema.items) + ) { + return getSchemaAlternatives(schema.items, ancestors); + } + const alternatives: | [AlternativeType, (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[]] | null = (() => { @@ -429,7 +509,91 @@ export function getSchemaAlternatives( } const [type, schemas] = alternatives; - return flattenAlternatives(type, schemas, new Set(ancestors).add(schema)); + return mergeAlternatives( + type, + flattenAlternatives(type, schemas, new Set(ancestors).add(schema)) + ); +} + +/** + * Merge alternatives of the same type into a single schema. + * - Merge string enums + */ +function mergeAlternatives( + alternativeType: AlternativeType, + schemasOrRefs: OpenAPIV3.SchemaObject[] +): OpenAPIV3.SchemaObject[] | null { + switch (alternativeType) { + case 'oneOf': { + return schemasOrRefs.reduce((acc, schemaOrRef) => { + const latest = acc.at(-1); + + if ( + latest && + latest.type === 'string' && + latest.enum && + schemaOrRef.type === 'string' && + schemaOrRef.enum + ) { + latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + + acc.push(schemaOrRef); + return acc; + }, []); + } + case 'allOf': { + return schemasOrRefs.reduce((acc, schemaOrRef) => { + const latest = acc.at(-1); + + if ( + latest && + latest.type === 'string' && + latest.enum && + schemaOrRef.type === 'string' && + schemaOrRef.enum + ) { + const keys = Object.keys(schemaOrRef); + if (keys.every((key) => ['type', 'enum', 'nullable'].includes(key))) { + latest.enum = Array.from(new Set([...latest.enum, ...schemaOrRef.enum])); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + } + + if (latest && latest.type === 'object' && schemaOrRef.type === 'object') { + const keys = Object.keys(schemaOrRef); + if ( + keys.every((key) => + ['type', 'properties', 'required', 'nullable'].includes(key) + ) + ) { + latest.properties = { + ...latest.properties, + ...schemaOrRef.properties, + }; + latest.required = Array.from( + new Set([ + ...(Array.isArray(latest.required) ? latest.required : []), + ...(Array.isArray(schemaOrRef.required) + ? schemaOrRef.required + : []), + ]) + ); + latest.nullable = latest.nullable || schemaOrRef.nullable; + return acc; + } + } + + acc.push(schemaOrRef); + return acc; + }, []); + } + default: + return schemasOrRefs; + } } function flattenAlternatives( @@ -437,6 +601,9 @@ function flattenAlternatives( schemasOrRefs: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], ancestors: Set ): OpenAPIV3.SchemaObject[] { + // Get the parent schema's required fields from the most recent ancestor + const latestAncestor = Array.from(ancestors).pop(); + return schemasOrRefs.reduce((acc, schemaOrRef) => { if (checkIsReference(schemaOrRef)) { return acc; @@ -445,16 +612,47 @@ function flattenAlternatives( if (schemaOrRef[alternativeType] && !ancestors.has(schemaOrRef)) { const schemas = getSchemaAlternatives(schemaOrRef, ancestors); if (schemas) { - acc.push(...schemas); + acc.push( + ...schemas.map((schema) => ({ + ...schema, + required: mergeRequiredFields(schema, latestAncestor), + })) + ); } return acc; } - acc.push(schemaOrRef); + // For direct schemas, handle required fields + const schema = { + ...schemaOrRef, + required: mergeRequiredFields(schemaOrRef, latestAncestor), + }; + + acc.push(schema); return acc; }, []); } +/** + * Merge the required fields of a schema with the required fields of its latest ancestor. + */ +function mergeRequiredFields( + schemaOrRef: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, + latestAncestor: OpenAPIV3.SchemaObject | undefined +) { + if (!schemaOrRef.required && !latestAncestor?.required) { + return undefined; + } + + if (checkIsReference(schemaOrRef)) { + return latestAncestor?.required; + } + + return Array.from( + new Set([...(latestAncestor?.required || []), ...(schemaOrRef.required || [])]) + ); +} + function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { // Otherwise try to infer a nice title let type = 'any'; @@ -472,6 +670,11 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { if (schema.format) { type += ` · ${schema.format}`; } + + // Only add the title if it's an object (no need for the title of a string, number, etc.) + if (type === 'object' && schema.title) { + type += ` · ${schema.title.replaceAll(' ', '')}`; + } } if ('anyOf' in schema) { @@ -486,20 +689,3 @@ function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { return type; } - -function getDisclosureLabel(schema: OpenAPIV3.SchemaObject): string { - if (schema.type === 'array' && !!schema.items) { - if (schema.items.oneOf) { - return 'available items'; - } - - // Fallback to "child attributes" for enums and objects - if (schema.items.enum || schema.items.type === 'object') { - return 'child attributes'; - } - - return schema.items.title ?? schema.title ?? getSchemaTitle(schema.items); - } - - return schema.title || 'child attributes'; -} diff --git a/packages/react-openapi/src/OpenAPISchemaName.tsx b/packages/react-openapi/src/OpenAPISchemaName.tsx index 1c66cd352..d24efe3f7 100644 --- a/packages/react-openapi/src/OpenAPISchemaName.tsx +++ b/packages/react-openapi/src/OpenAPISchemaName.tsx @@ -1,12 +1,14 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import type React from 'react'; -import { stringifyOpenAPI } from './stringifyOpenAPI'; +import type { OpenAPIClientContext } from './context'; +import { t, tString } from './translate'; interface OpenAPISchemaNameProps { schema?: OpenAPIV3.SchemaObject; propertyName?: string | React.JSX.Element; - required?: boolean; + required?: boolean | null; type?: string; + context: OpenAPIClientContext; } /** @@ -14,12 +16,12 @@ interface OpenAPISchemaNameProps { * It includes the property name, type, required and deprecated status. */ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { - const { schema, type, propertyName, required } = props; + const { schema, type, propertyName, required, context } = props; - const additionalItems = schema && getAdditionalItems(schema); + const additionalItems = schema && getAdditionalItems(schema, context); return ( -
+ {propertyName ? ( {propertyName} @@ -31,38 +33,45 @@ export function OpenAPISchemaName(props: OpenAPISchemaNameProps) { {additionalItems} ) : null} - {schema?.readOnly ? read-only : null} + {schema?.readOnly ? ( + + {t(context.translation, 'read_only')} + + ) : null} {schema?.writeOnly ? ( - write-only + + {t(context.translation, 'write_only')} + ) : null} - {required ? ( - required + {required === null ? null : required ? ( + + {t(context.translation, 'required')} + ) : ( - optional + + {t(context.translation, 'optional')} + )} - {schema?.deprecated ? Deprecated : null} -
+ {schema?.deprecated ? ( + {t(context.translation, 'deprecated')} + ) : null} + ); } -function getAdditionalItems(schema: OpenAPIV3.SchemaObject): string { +function getAdditionalItems(schema: OpenAPIV3.SchemaObject, context: OpenAPIClientContext): string { let additionalItems = ''; if (schema.minimum || schema.minLength || schema.minItems) { - additionalItems += ` · min: ${schema.minimum || schema.minLength || schema.minItems}`; + additionalItems += ` · ${tString(context.translation, 'min').toLowerCase()}: ${schema.minimum || schema.minLength || schema.minItems}`; } if (schema.maximum || schema.maxLength || schema.maxItems) { - additionalItems += ` · max: ${schema.maximum || schema.maxLength || schema.maxItems}`; - } - - // If the schema has a default value, we display it - if (typeof schema.default !== 'undefined') { - additionalItems += ` · default: ${stringifyOpenAPI(schema.default)}`; + additionalItems += ` · ${tString(context.translation, 'max').toLowerCase()}: ${schema.maximum || schema.maxLength || schema.maxItems}`; } if (schema.nullable) { - additionalItems = ' | nullable'; + additionalItems = ` | ${tString(context.translation, 'nullable').toLowerCase()}`; } return additionalItems; diff --git a/packages/react-openapi/src/OpenAPISchemaServer.tsx b/packages/react-openapi/src/OpenAPISchemaServer.tsx index d2396c705..2c3fd66da 100644 --- a/packages/react-openapi/src/OpenAPISchemaServer.tsx +++ b/packages/react-openapi/src/OpenAPISchemaServer.tsx @@ -4,8 +4,8 @@ import { OpenAPISchemaPropertiesFromServer, type OpenAPISchemaPropertyEntry, } from './OpenAPISchema'; +import type { OpenAPIClientContext } from './context'; import { decycle } from './decycle'; -import type { OpenAPIClientContext } from './types'; export function OpenAPISchemaProperties(props: { id?: string; diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx index 465afeb08..3f8623650 100644 --- a/packages/react-openapi/src/OpenAPISecurities.tsx +++ b/packages/react-openapi/src/OpenAPISecurities.tsx @@ -1,9 +1,10 @@ -import type { OpenAPIV3_1 } from '@gitbook/openapi-parser'; import { InteractiveSection } from './InteractiveSection'; import { Markdown } from './Markdown'; import { OpenAPISchemaName } from './OpenAPISchemaName'; -import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; -import { resolveDescription } from './utils'; +import type { OpenAPIClientContext } from './context'; +import { t } from './translate'; +import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types'; +import { createStateKey, resolveDescription } from './utils'; /** * Present securities authorization that can be used for this operation. @@ -20,10 +21,12 @@ export function OpenAPISecurities(props: { return ( { const description = resolveDescription(security); @@ -33,7 +36,7 @@ export function OpenAPISecurities(props: { body: (
- {getLabelForType(security)} + {getLabelForType(security, context)} {description ? ( ); case 'http': if (security.scheme === 'basic') { - return ; + return ( + + ); } if (security.scheme === 'bearer') { const description = resolveDescription(security); return ( <> - + {/** Show a default description if none is provided */} {!description ? ( ; + return ( + + ); case 'oauth2': - return ; + return ( + + ); case 'openIdConnect': - return ; + return ( + + ); default: // @ts-ignore return security.type; diff --git a/packages/react-openapi/src/OpenAPISelect.tsx b/packages/react-openapi/src/OpenAPISelect.tsx new file mode 100644 index 000000000..e8551e9b9 --- /dev/null +++ b/packages/react-openapi/src/OpenAPISelect.tsx @@ -0,0 +1,96 @@ +'use client'; + +import clsx from 'clsx'; +import { useCallback } from 'react'; +import { + Button, + type Key, + ListBox, + ListBoxItem, + type ListBoxItemProps, + Popover, + type PopoverProps, + Select, + type SelectProps, + SelectValue, +} from 'react-aria-components'; +import { useStore } from 'zustand'; +import { getOrCreateStoreByKey } from './getOrCreateStoreByKey'; + +export type OpenAPISelectItem = { + key: Key; + label: string | React.ReactNode; +}; + +interface OpenAPISelectProps extends Omit, 'children'> { + items: T[]; + children: React.ReactNode | ((item: T) => React.ReactNode); + placement?: PopoverProps['placement']; + stateKey?: string; + /** + * Icon to display in the select button. + */ + icon?: React.ReactNode; +} + +export function useSelectState(stateKey = 'select-state', initialKey?: Key) { + const store = useStore(getOrCreateStoreByKey(stateKey, initialKey)); + return { + key: store.key, + setKey: useCallback((key: Key | null) => store.setKey(key), [store.setKey]), + }; +} + +export function OpenAPISelect(props: OpenAPISelectProps) { + const { + icon = '▼', + items, + children, + className, + placement, + stateKey, + selectedKey, + onSelectionChange, + } = props; + + const state = useSelectState(stateKey, items[0]?.key); + + const selected = items.find((item) => item.key === state.key) || items[0]; + + return ( + + ); +} + +export function OpenAPISelectItem(props: ListBoxItemProps) { + return ( + + clsx('openapi-select-item', { + 'openapi-select-item-focused': isFocused, + 'openapi-select-item-selected': isSelected, + }) + } + /> + ); +} diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index 599923656..49c41cda4 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -5,16 +5,23 @@ import { OpenAPIResponses } from './OpenAPIResponses'; import { OpenAPISchemaProperties } from './OpenAPISchemaServer'; import { OpenAPISecurities } from './OpenAPISecurities'; import { StaticSection } from './StaticSection'; -import type { OpenAPIClientContext, OpenAPIOperationData } from './types'; +import type { OpenAPIClientContext } from './context'; +import { tString } from './translate'; +import type { OpenAPIOperationData, OpenAPIWebhookData } from './types'; import { parameterToProperty } from './utils'; -export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAPIClientContext }) { +export function OpenAPISpec(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIClientContext; +}) { const { data, context } = props; - const { operation, securities } = data; + const { operation } = data; const parameters = operation.parameters ?? []; - const parameterGroups = groupParameters(parameters); + const parameterGroups = groupParameters(parameters, context); + + const securities = 'securities' in data ? data.securities : []; return ( <> @@ -42,6 +49,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP key="body" requestBody={operation.requestBody} context={context} + data={data} /> ) : null} {operation.responses ? ( @@ -55,7 +63,10 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP ); } -function groupParameters(parameters: OpenAPI.Parameters): Array<{ +function groupParameters( + parameters: OpenAPI.Parameters, + context: OpenAPIClientContext +): Array<{ key: string; label: string; parameters: OpenAPI.Parameters; @@ -72,7 +83,7 @@ function groupParameters(parameters: OpenAPI.Parameters): Array<{ .filter((parameter) => parameter.in) .forEach((parameter) => { const key = parameter.in; - const label = getParameterGroupName(parameter.in); + const label = getParameterGroupName(parameter.in, context); const group = groups.find((group) => group.key === key); if (group) { group.parameters.push(parameter); @@ -90,14 +101,14 @@ function groupParameters(parameters: OpenAPI.Parameters): Array<{ return groups; } -function getParameterGroupName(paramIn: string): string { +function getParameterGroupName(paramIn: string, context: OpenAPIClientContext): string { switch (paramIn) { case 'path': - return 'Path parameters'; + return tString(context.translation, 'path_parameters'); case 'query': - return 'Query parameters'; + return tString(context.translation, 'query_parameters'); case 'header': - return 'Header parameters'; + return tString(context.translation, 'header_parameters'); default: return paramIn; } diff --git a/packages/react-openapi/src/OpenAPITabs.tsx b/packages/react-openapi/src/OpenAPITabs.tsx index 50fd6f63e..f3502ba1d 100644 --- a/packages/react-openapi/src/OpenAPITabs.tsx +++ b/packages/react-openapi/src/OpenAPITabs.tsx @@ -3,7 +3,7 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { type Key, Tab, TabList, TabPanel, Tabs, type TabsProps } from 'react-aria-components'; import { useEventCallback } from 'usehooks-ts'; -import { getOrCreateTabStoreByKey } from './useSyncedTabsGlobalState'; +import { getOrCreateStoreByKey } from './getOrCreateStoreByKey'; export type TabItem = { key: Key; @@ -36,8 +36,8 @@ export function OpenAPITabs( const { children, items, stateKey } = props; const [tabKey, setTabKey] = useState(() => { if (stateKey && typeof window !== 'undefined') { - const store = getOrCreateTabStoreByKey(stateKey); - const tabKey = store.getState().tabKey; + const store = getOrCreateStoreByKey(stateKey); + const tabKey = store.getState().key; if (tabKey) { return tabKey; } @@ -60,10 +60,10 @@ export function OpenAPITabs( if (!stateKey) { return undefined; } - const store = getOrCreateTabStoreByKey(stateKey); + const store = getOrCreateStoreByKey(stateKey); return store.subscribe((state) => { cancelDeferRef.current?.(); - cancelDeferRef.current = defer(() => selectTab(state.tabKey)); + cancelDeferRef.current = defer(() => selectTab(state.key)); }); }, [stateKey, selectTab]); useEffect(() => { @@ -77,8 +77,8 @@ export function OpenAPITabs( onSelectionChange={(tabKey) => { selectTab(tabKey); if (stateKey) { - const store = getOrCreateTabStoreByKey(stateKey); - store.setState({ tabKey }); + const store = getOrCreateStoreByKey(stateKey); + store.setState({ key: tabKey }); } }} selectedKey={tabKey} @@ -138,9 +138,9 @@ export function OpenAPITabsPanels() { return ( -
{selectedTab.body}
+
{selectedTab.body}
{selectedTab.footer ? ( -
{selectedTab.footer}
+
{selectedTab.footer}
) : null}
); diff --git a/packages/react-openapi/src/OpenAPIWebhook.tsx b/packages/react-openapi/src/OpenAPIWebhook.tsx new file mode 100644 index 000000000..c81d57ea4 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIWebhook.tsx @@ -0,0 +1,33 @@ +import clsx from 'clsx'; +import { OpenAPIWebhookExample } from './OpenAPIWebhookExample'; +import { OpenAPIColumnSpec } from './common/OpenAPIColumnSpec'; +import { OpenAPISummary } from './common/OpenAPISummary'; +import { type OpenAPIContextInput, resolveOpenAPIContext } from './context'; +import type { OpenAPIWebhookData } from './types'; + +/** + * Display an interactive OpenAPI webhook. + */ +export function OpenAPIWebhook(props: { + className?: string; + data: OpenAPIWebhookData; + context: OpenAPIContextInput; +}) { + const { className, data, context: contextInput } = props; + + const context = resolveOpenAPIContext(contextInput); + + return ( +
+ +
+ +
+
+ +
+
+
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIWebhookExample.tsx b/packages/react-openapi/src/OpenAPIWebhookExample.tsx new file mode 100644 index 000000000..5e66cfb56 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIWebhookExample.tsx @@ -0,0 +1,60 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIEmptyExample } from './OpenAPIExample'; +import { OpenAPIMediaTypeContent } from './OpenAPIMediaType'; +import { type OpenAPIContext, getOpenAPIClientContext } from './context'; +import type { OpenAPIWebhookData } from './types'; +import { getExamples } from './util/example'; +import { createStateKey } from './utils'; + +export function OpenAPIWebhookExample(props: { + data: OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const items = (() => { + if (!operation.requestBody) { + return []; + } + + return Object.entries( + operation.requestBody.content as Record + ).map(([key, value]) => { + const schema = value.schema; + + if (!schema) { + return { + key, + label: key, + body: , + }; + } + + return { + key, + label: key, + body: <>, + examples: getExamples({ + mediaTypeObject: value, + mediaType: key, + context, + }), + }; + }); + })(); + + return ( +
+

Payload

+
+ +
+
+ ); +} diff --git a/packages/react-openapi/src/ScalarApiButton.tsx b/packages/react-openapi/src/ScalarApiButton.tsx index 04d7bb1aa..59024d202 100644 --- a/packages/react-openapi/src/ScalarApiButton.tsx +++ b/packages/react-openapi/src/ScalarApiButton.tsx @@ -6,6 +6,8 @@ import { createPortal } from 'react-dom'; import type { OpenAPIV3_1 } from '@gitbook/openapi-parser'; import { useOpenAPIOperationContext } from './OpenAPIOperationContext'; +import type { OpenAPIClientContext } from './context'; +import { t } from './translate'; /** * Button which launches the Scalar API Client @@ -14,8 +16,9 @@ export function ScalarApiButton(props: { method: OpenAPIV3_1.HttpMethods; path: string; specUrl: string; + context: OpenAPIClientContext; }) { - const { method, path, specUrl } = props; + const { method, path, specUrl, context } = props; const [isOpen, setIsOpen] = useState(false); const controllerRef = useRef(null); return ( @@ -27,7 +30,7 @@ export function ScalarApiButton(props: { setIsOpen(true); }} > - Test it + {t(context.translation, 'test_it')} + ); diff --git a/packages/react-openapi/src/StaticSection.tsx b/packages/react-openapi/src/StaticSection.tsx index 5124227e7..b79b20e61 100644 --- a/packages/react-openapi/src/StaticSection.tsx +++ b/packages/react-openapi/src/StaticSection.tsx @@ -11,7 +11,7 @@ export function SectionHeader(props: ComponentPropsWithoutRef<'div'>) { {...props} className={clsx( 'openapi-section-header', - props.className && `${props.className}-header` + props.className ? `${props.className}-header` : undefined )} /> ); @@ -42,18 +42,50 @@ export const SectionBody = forwardRef(function SectionBody( ); }); +export function SectionFooter(props: ComponentPropsWithoutRef<'div'>) { + return ( +
+ ); +} + +export function SectionFooterContent(props: ComponentPropsWithoutRef<'div'>) { + return ( +
+ ); +} + export function StaticSection(props: { className: string; - header: React.ReactNode; + header?: React.ReactNode; children: React.ReactNode; + footer?: React.ReactNode; }) { - const { className, header, children } = props; + const { className, header, children, footer } = props; return (
- - {header} - + {header ? ( + + {header} + + ) : null} {children} + {footer ? ( + + {footer} + + ) : null}
); } diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts index adcb3922a..7fd24bb3f 100644 --- a/packages/react-openapi/src/code-samples.test.ts +++ b/packages/react-openapi/src/code-samples.test.ts @@ -413,13 +413,16 @@ describe('python code sample generator', () => { }, body: { key: 'value', + truethy: true, + falsey: false, + nullish: null, }, }; const output = generator?.generate(input); expect(output).toBe( - 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data={"key":"value"}\n)\n\ndata = response.json()' + 'import requests\n\nresponse = requests.get(\n "https://example.com/path",\n headers={"Content-Type":"application/json"},\n data=json.dumps({"key":"value","truethy":True,"falsey":False,"nullish":None})\n)\n\ndata = response.json()' ); }); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 44d29b95b..50c6a9204 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -3,6 +3,7 @@ import { isFormData, isFormUrlEncoded, isGraphQL, + isJSON, isPDF, isPlainObject, isText, @@ -26,6 +27,52 @@ export interface CodeSampleGenerator { } export const codeSampleGenerators: CodeSampleGenerator[] = [ + { + id: 'http', + label: 'HTTP', + syntax: 'http', + generate: ({ method, url, headers = {}, body }: CodeSampleInput) => { + const { host, path } = parseHostAndPath(url); + + if (body) { + // if we had a body add a content length header + const bodyContent = body ? stringifyOpenAPI(body) : ''; + // handle unicode chars with a text encoder + const encoder = new TextEncoder(); + + const bodyString = BodyGenerators.getHTTPBody(body, headers); + + if (bodyString) { + body = bodyString; + } + + headers = { + ...headers, + 'Content-Length': encoder.encode(bodyContent).length.toString(), + }; + } + + if (!headers.hasOwnProperty('Accept')) { + headers.Accept = '*/*'; + } + + const headerString = headers + ? `${Object.entries(headers) + .map(([key, value]) => + key.toLowerCase() !== 'host' ? `${key}: ${value}` : '' + ) + .join('\n')}\n` + : ''; + + const bodyString = body ? `\n${body}` : ''; + + const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 +Host: ${host} +${headerString}${bodyString}`; + + return httpRequest; + }, + }, { id: 'curl', label: 'cURL', @@ -127,11 +174,14 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4); } + const contentType = headers?.['Content-Type']; if (body) { if (body === 'files') { code += indent(`files=${body}\n`, 4); + } else if (isJSON(contentType)) { + code += indent(`data=json.dumps(${body})\n`, 4); } else { - code += indent(`data=${stringifyOpenAPI(body)}\n`, 4); + code += indent(`data=${body}\n`, 4); } } @@ -140,52 +190,6 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ return code; }, }, - { - id: 'http', - label: 'HTTP', - syntax: 'bash', - generate: ({ method, url, headers = {}, body }: CodeSampleInput) => { - const { host, path } = parseHostAndPath(url); - - if (body) { - // if we had a body add a content length header - const bodyContent = body ? stringifyOpenAPI(body) : ''; - // handle unicode chars with a text encoder - const encoder = new TextEncoder(); - - const bodyString = BodyGenerators.getHTTPBody(body, headers); - - if (bodyString) { - body = bodyString; - } - - headers = { - ...headers, - 'Content-Length': encoder.encode(bodyContent).length.toString(), - }; - } - - if (!headers.hasOwnProperty('Accept')) { - headers.Accept = '*/*'; - } - - const headerString = headers - ? `${Object.entries(headers) - .map(([key, value]) => - key.toLowerCase() !== 'host' ? `${key}: ${value}` : '' - ) - .join('\n')}\n` - : ''; - - const bodyString = body ? `\n${body}` : ''; - - const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 -Host: ${host} -${headerString}${bodyString}`; - - return httpRequest; - }, - }, ]; function indent(code: string, spaces: number) { @@ -343,18 +347,30 @@ const BodyGenerators = { } code += '}\n\n'; body = 'files'; - } - - if (isPDF(contentType)) { + } else if (isPDF(contentType)) { code += 'files = {\n'; code += `${indent(`"file": "${body}",`, 4)}\n`; code += '}\n\n'; body = 'files'; - } - - if (isXML(contentType)) { + } else if (isXML(contentType)) { // Convert JSON to XML if needed - body = convertBodyToXML(body); + body = JSON.stringify(convertBodyToXML(body)); + } else { + body = stringifyOpenAPI(body, (_key, value) => { + switch (value) { + case true: + return '$$__TRUE__$$'; + case false: + return '$$__FALSE__$$'; + case null: + return '$$__NULL__$$'; + default: + return value; + } + }) + .replaceAll('"$$__TRUE__$$"', 'True') + .replaceAll('"$$__FALSE__$$"', 'False') + .replaceAll('"$$__NULL__$$"', 'None'); } return { body, code, headers }; diff --git a/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx b/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx new file mode 100644 index 000000000..59eb8d539 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIColumnSpec.tsx @@ -0,0 +1,31 @@ +import { OpenAPISpec } from '../OpenAPISpec'; +import { type OpenAPIContext, getOpenAPIClientContext } from '../context'; +import { t } from '../translate'; +import type { OpenAPIOperationData, OpenAPIWebhookData } from '../types'; +import { OpenAPIOperationDescription } from './OpenAPIOperationDescription'; + +export function OpenAPIColumnSpec(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const clientContext = getOpenAPIClientContext(context); + + return ( +
+ {operation['x-deprecated-sunset'] ? ( +
+ {t(context.translation, 'deprecated_and_sunset_on', [ + + {operation['x-deprecated-sunset']} + , + ])} +
+ ) : null} + + +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx new file mode 100644 index 000000000..53c827523 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIOperationDescription.tsx @@ -0,0 +1,31 @@ +import type { OpenAPICustomOperationProperties, OpenAPIV3 } from '@gitbook/openapi-parser'; +import { Markdown } from '../Markdown'; +import type { OpenAPIContext } from '../context'; +import { resolveDescription } from '../utils'; + +export function OpenAPIOperationDescription(props: { + operation: OpenAPIV3.OperationObject; + context: OpenAPIContext; +}) { + const { operation } = props; + if (operation['x-gitbook-description-document']) { + return ( +
+ {props.context.renderDocument({ + document: operation['x-gitbook-description-document'], + })} +
+ ); + } + + const description = resolveDescription(operation); + if (!description) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPIStability.tsx b/packages/react-openapi/src/common/OpenAPIStability.tsx new file mode 100644 index 000000000..5e6234f41 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIStability.tsx @@ -0,0 +1,23 @@ +import type { OpenAPIStability as OpenAPIStabilityType } from '@gitbook/openapi-parser'; + +const stabilityEnum: Record = { + experimental: 'Experimental', + alpha: 'Alpha', + beta: 'Beta', +} as const; + +export function OpenAPIStability(props: { stability: OpenAPIStabilityType }) { + const { stability } = props; + + const foundStability = stabilityEnum[stability]; + + if (!foundStability) { + return null; + } + + return ( +
+ {foundStability} +
+ ); +} diff --git a/packages/react-openapi/src/common/OpenAPISummary.tsx b/packages/react-openapi/src/common/OpenAPISummary.tsx new file mode 100644 index 000000000..8c81ae2b4 --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPISummary.tsx @@ -0,0 +1,45 @@ +import { OpenAPIPath } from '../OpenAPIPath'; +import type { OpenAPIContext } from '../context'; +import type { OpenAPIOperationData, OpenAPIWebhookData } from '../types'; +import { OpenAPIStability } from './OpenAPIStability'; + +export function OpenAPISummary(props: { + data: OpenAPIOperationData | OpenAPIWebhookData; + context: OpenAPIContext; +}) { + const { data, context } = props; + const { operation } = data; + + const title = (() => { + if (operation.summary) { + return operation.summary; + } + + if ('name' in data) { + return data.name; + } + + return undefined; + })(); + + return ( +
+ {(operation.deprecated || operation['x-stability']) && ( +
+ {operation.deprecated &&
Deprecated
} + {operation['x-stability'] && ( + + )} +
+ )} + {title + ? context.renderHeading({ + deprecated: operation.deprecated ?? false, + stability: operation['x-stability'], + title, + }) + : null} + {'path' in data ? : null} +
+ ); +} diff --git a/packages/react-openapi/src/context.ts b/packages/react-openapi/src/context.ts new file mode 100644 index 000000000..cf7519034 --- /dev/null +++ b/packages/react-openapi/src/context.ts @@ -0,0 +1,99 @@ +import { type Translation, type TranslationLocale, translations } from './translations'; + +export interface OpenAPIClientContext { + /** + * The translation language to use. + */ + translation: Translation; + + /** + * Icons used in the block. + */ + icons: { + chevronDown: React.ReactNode; + chevronRight: React.ReactNode; + plus: React.ReactNode; + }; + + /** + * Force all sections to be opened by default. + * @default false + */ + defaultInteractiveOpened?: boolean; + + /** + * The key of the block + */ + blockKey?: string; + + /** + * Optional id attached to the heading and used as an anchor. + */ + id?: string; + + /** + * Mark the context as a client context. + */ + $$isClientContext$$: true; +} + +export interface OpenAPIContext extends Omit { + /** + * Render a code block. + */ + renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode; + + /** + * Render the heading of the operation. + */ + renderHeading: (props: { + deprecated: boolean; + title: string; + stability?: string; + }) => React.ReactNode; + + /** + * Render the document of the operation. + */ + renderDocument: (props: { document: object }) => React.ReactNode; + + /** + * Specification URL. + */ + specUrl: string; +} + +export type OpenAPIUniversalContext = OpenAPIClientContext | OpenAPIContext; + +export interface OpenAPIContextInput extends Omit { + /** + * The translation language to use. + * @default 'en' + */ + locale?: TranslationLocale | undefined; +} + +/** + * Resolve OpenAPI context from the input. + */ +export function resolveOpenAPIContext(context: OpenAPIContextInput): OpenAPIContext { + const { locale, ...rest } = context; + return { + ...rest, + translation: translations[locale ?? 'en'], + }; +} + +/** + * Get the client context from the OpenAPI context. + */ +export function getOpenAPIClientContext(context: OpenAPIUniversalContext): OpenAPIClientContext { + return { + translation: context.translation, + icons: context.icons, + defaultInteractiveOpened: context.defaultInteractiveOpened, + blockKey: context.blockKey, + id: context.id, + $$isClientContext$$: true, + }; +} diff --git a/packages/react-openapi/src/generateSchemaExample.test.ts b/packages/react-openapi/src/generateSchemaExample.test.ts new file mode 100644 index 000000000..3181881b6 --- /dev/null +++ b/packages/react-openapi/src/generateSchemaExample.test.ts @@ -0,0 +1,1020 @@ +import { describe, expect, it } from 'bun:test'; + +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { generateSchemaExample } from './generateSchemaExample'; + +describe('generateSchemaExample', () => { + it('sets example values', () => { + expect( + generateSchemaExample({ + example: 10, + }) + ).toBe(10); + }); + + it('uses first example, if multiple are configured', () => { + expect( + generateSchemaExample({ + examples: [10], + }) + ).toBe(10); + }); + + it('takes the first enum as example', () => { + expect( + generateSchemaExample({ + enum: ['available', 'pending', 'sold'], + }) + ).toBe('available'); + }); + + it('uses "text" as a fallback for strings', () => { + expect( + generateSchemaExample({ + type: 'string', + }) + ).toBe('text'); + }); + + it('only includes required attributes and attributes with example values', () => { + expect( + generateSchemaExample( + { + type: 'object', + required: ['first_name'], + properties: { + first_name: { + type: 'string', + }, + last_name: { + type: 'string', + required: true, + }, + position: { + type: 'string', + examples: ['Developer'], + }, + description: { + type: 'string', + example: 'A developer', + }, + age: { + type: 'number', + }, + }, + }, + { + omitEmptyAndOptionalProperties: true, + } + ) + ).toStrictEqual({ + first_name: 'text', + last_name: 'text', + position: 'Developer', + description: 'A developer', + }); + }); + + it('includes every available attributes', () => { + expect( + generateSchemaExample( + { + type: 'object', + required: ['first_name'], + properties: { + first_name: { + type: 'string', + }, + last_name: { + type: 'string', + required: true, + }, + position: { + type: 'string', + examples: ['Developer'], + }, + description: { + type: 'string', + example: 'A developer', + }, + age: { + type: 'number', + }, + }, + }, + { + omitEmptyAndOptionalProperties: false, + } + ) + ).toStrictEqual({ + first_name: 'text', + last_name: 'text', + position: 'Developer', + description: 'A developer', + age: 1, + }); + }); + + it('uses example value for first type in non-null union types', () => { + expect( + generateSchemaExample({ + type: ['string', 'number'], + } as OpenAPIV3.BaseSchemaObject) + ).toBe('text'); + }); + + it('uses null for nullable union types', () => { + expect( + generateSchemaExample({ + type: ['string', 'null'], + } as OpenAPIV3.BaseSchemaObject) + ).toBeNull(); + }); + + it('sets example values', () => { + expect( + generateSchemaExample({ + example: 10, + }) + ).toBe(10); + }); + + it('goes through properties recursively with objects', () => { + expect( + generateSchemaExample({ + type: 'object', + properties: { + category: { + type: 'object', + properties: { + id: { + example: 1, + }, + name: { + example: 'Dogs', + }, + attributes: { + type: 'object', + properties: { + size: { + enum: ['small', 'medium', 'large'], + }, + }, + }, + }, + }, + }, + }) + ).toMatchObject({ + category: { + id: 1, + name: 'Dogs', + attributes: { + size: 'small', + }, + }, + }); + }); + + it('goes through properties recursively with arrays', () => { + expect( + generateSchemaExample({ + type: 'object', + properties: { + tags: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + example: 1, + }, + }, + }, + }, + }, + }) + ).toMatchObject({ + tags: [ + { + id: 1, + }, + ], + }); + }); + + it('uses empty [] as a fallback for arrays', () => { + expect( + generateSchemaExample({ + type: 'object', + properties: { + title: { + type: 'array', + }, + }, + }) + ).toMatchObject({ + title: [], + }); + }); + + // it('returns emails as an example value', () => { + // const result = generateSchemaExample({ + // type: 'string', + // format: 'email', + // }); + + // function isEmail(text: string) { + // return !!text.match(/^.+@.+\..+$/); + // } + + // expect(isEmail(result)).toBe(true); + // }); + + it('uses true as a fallback for booleans', () => { + expect( + generateSchemaExample({ + type: 'boolean', + }) + ).toBe(true); + }); + + it('uses 1 as a fallback for integers', () => { + expect( + generateSchemaExample({ + type: 'integer', + }) + ).toBe(1); + }); + + it('returns an array if the schema type is array', () => { + expect( + generateSchemaExample({ + type: 'array', + }) + ).toMatchObject([]); + }); + + it('uses array example values', () => { + expect( + generateSchemaExample({ + type: 'array', + example: ['foobar'], + items: { + type: 'string', + }, + }) + ).toMatchObject(['foobar']); + }); + + it('uses specified object as array default', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + type: 'object', + properties: { + foo: { + type: 'number', + }, + bar: { + type: 'string', + }, + }, + }, + }) + ).toMatchObject([ + { + foo: 1, + bar: 'text', + }, + ]); + }); + + it('uses the first example in object anyOf', () => { + expect( + generateSchemaExample({ + type: 'object', + anyOf: [ + { + type: 'object', + properties: { + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }) + ).toMatchObject({ foo: 1 }); + }); + + it('uses the first example in object oneOf', () => { + expect( + generateSchemaExample({ + type: 'object', + oneOf: [ + { + type: 'object', + properties: { + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }) + ).toMatchObject({ foo: 1 }); + }); + + it('uses the first example in object anyOf when type is not defined', () => { + expect( + generateSchemaExample({ + anyOf: [ + { + type: 'object', + properties: { + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }) + ).toMatchObject({ foo: 1 }); + }); + + it('uses the first example in object oneOf when type is not defined', () => { + expect( + generateSchemaExample({ + oneOf: [ + { + type: 'object', + properties: { + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }) + ).toMatchObject({ foo: 1 }); + }); + + it('uses all examples in object allOf', () => { + expect( + generateSchemaExample({ + allOf: [ + { + type: 'object', + properties: { + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }) + ).toMatchObject({ foo: 1, bar: 'text' }); + }); + + it('merges allOf items in arrays', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + allOf: [ + { + type: 'object', + properties: { + foobar: { type: 'string' }, + foo: { type: 'number' }, + }, + }, + { + type: 'object', + properties: { + bar: { type: 'string' }, + }, + }, + ], + }, + }) + ).toMatchObject([{ foobar: 'text', foo: 1, bar: 'text' }]); + }); + + it('handles array items with allOf containing objects', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + allOf: [ + { + type: 'object', + properties: { + id: { type: 'number', example: 1 }, + }, + }, + { + type: 'object', + properties: { + name: { type: 'string', example: 'test' }, + }, + }, + ], + }, + }) + ).toMatchObject([ + { + id: 1, + name: 'test', + }, + ]); + }); + + it('uses the first example in array anyOf', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + anyOf: [ + { + type: 'string', + example: 'foobar', + }, + { + type: 'string', + example: 'barfoo', + }, + ], + }, + }) + ).toMatchObject(['foobar']); + }); + + it('uses one example in array oneOf', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + oneOf: [ + { + type: 'string', + example: 'foobar', + }, + { + type: 'string', + example: 'barfoo', + }, + ], + }, + }) + ).toMatchObject(['foobar']); + }); + + it('uses all examples in array allOf', () => { + expect( + generateSchemaExample({ + type: 'array', + items: { + allOf: [ + { + type: 'string', + example: 'foobar', + }, + { + type: 'string', + example: 'barfoo', + }, + ], + }, + }) + ).toMatchObject(['foobar', 'barfoo']); + }); + + it('uses 1 as the default for a number', () => { + expect( + generateSchemaExample({ + type: 'number', + }) + ).toBe(1); + }); + + it('uses min as the default for a number', () => { + expect( + generateSchemaExample({ + type: 'number', + min: 200, + }) + ).toBe(200); + }); + + it('returns plaintext', () => { + expect( + generateSchemaExample({ + type: 'string', + example: 'foobar', + }) + ).toEqual('foobar'); + }); + + it('converts a whole schema to an example response', () => { + const schema: OpenAPIV3.SchemaObject = { + required: ['name', 'photoUrls'], + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int64', + example: 10, + }, + name: { + type: 'string', + example: 'doggie', + }, + category: { + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int64', + example: 1, + }, + name: { + type: 'string', + example: 'Dogs', + }, + }, + xml: { + name: 'category', + }, + }, + photoUrls: { + type: 'array', + xml: { + wrapped: true, + }, + items: { + type: 'string', + xml: { + name: 'photoUrl', + }, + }, + }, + tags: { + type: 'array', + xml: { + wrapped: true, + }, + items: { + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int64', + }, + name: { + type: 'string', + }, + }, + xml: { + name: 'tag', + }, + }, + }, + status: { + type: 'string', + description: 'pet status in the store', + enum: ['available', 'pending', 'sold'], + }, + }, + xml: { + name: 'pet', + }, + }; + + expect(generateSchemaExample(schema)).toMatchObject({ + id: 10, + name: 'doggie', + category: { + id: 1, + name: 'Dogs', + }, + photoUrls: ['text'], + tags: [ + { + id: 1, + name: 'text', + }, + ], + status: 'available', + }); + }); + + it('outputs XML', () => { + expect( + generateSchemaExample( + { + type: 'object', + properties: { + id: { + example: 1, + xml: { + name: 'foo', + }, + }, + }, + }, + { xml: true } + ) + ).toMatchObject({ + foo: 1, + }); + }); + + it('add XML wrappers where needed', () => { + expect( + generateSchemaExample( + { + type: 'object', + properties: { + photoUrls: { + type: 'array', + xml: { + wrapped: true, + }, + items: { + type: 'string', + example: 'https://example.com', + xml: { + name: 'photoUrl', + }, + }, + }, + }, + }, + { xml: true } + ) + ).toMatchObject({ + photoUrls: [{ photoUrl: 'https://example.com' }], + }); + }); + + it('doesn’t wrap items when not needed', () => { + expect( + generateSchemaExample( + { + type: 'object', + properties: { + photoUrls: { + type: 'array', + items: { + type: 'string', + example: 'https://example.com', + xml: { + name: 'photoUrl', + }, + }, + }, + }, + }, + { xml: true } + ) + ).toMatchObject({ + photoUrls: ['https://example.com'], + }); + }); + + it('use the first item of oneOf', () => { + expect( + generateSchemaExample({ + oneOf: [ + { + maxLength: 255, + type: 'string', + }, + { + type: 'null', + }, + ], + }) + ).toBe('text'); + }); + + it('works with allOf', () => { + expect( + generateSchemaExample({ + allOf: [ + { + type: 'string', + }, + ], + }) + ).toBe('text'); + }); + + it('uses all schemas in allOf', () => { + expect( + generateSchemaExample({ + allOf: [ + { + type: 'object', + properties: { + id: { + example: 10, + }, + }, + }, + { + type: 'object', + properties: { + title: { + example: 'Foobar', + }, + }, + }, + ], + }) + ).toMatchObject({ + id: 10, + title: 'Foobar', + }); + }); + + it('returns null for unknown types', () => { + expect( + generateSchemaExample({ + type: 'fantasy', + } as OpenAPIV3.BaseSchemaObject) + ).toBe(null); + }); + + it('returns readOnly attributes by default', () => { + expect( + generateSchemaExample({ + example: 'foobar', + readOnly: true, + }) + ).toBe('foobar'); + }); + + it('returns readOnly attributes in read mode', () => { + expect( + generateSchemaExample( + { + example: 'foobar', + readOnly: true, + }, + { + mode: 'read', + } + ) + ).toBe('foobar'); + }); + + it('doesn’t return readOnly attributes in write mode', () => { + expect( + generateSchemaExample( + { + example: 'foobar', + readOnly: true, + }, + { + mode: 'write', + } + ) + ).toBeUndefined(); + }); + + it('returns writeOnly attributes by default', () => { + expect( + generateSchemaExample({ + example: 'foobar', + writeOnly: true, + }) + ).toBe('foobar'); + }); + + it('returns writeOnly attributes in write mode', () => { + expect( + generateSchemaExample( + { + example: 'foobar', + writeOnly: true, + }, + { + mode: 'write', + } + ) + ).toBe('foobar'); + }); + + it('doesn’t return writeOnly attributes in read mode', () => { + expect( + generateSchemaExample( + { + example: 'foobar', + writeOnly: true, + }, + { + mode: 'read', + } + ) + ).toBeUndefined(); + }); + + it('allows any additonalProperty', () => { + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: {}, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: 'anything', + }); + + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: true, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: 'anything', + }); + }); + + it('adds an additionalProperty with specific types', () => { + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: { + type: 'integer', + }, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: 1, + }); + + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: { + type: 'boolean', + }, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: true, + }); + + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: { + type: 'string', + }, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: 'text', + }); + + expect( + generateSchemaExample({ + type: 'object', + additionalProperties: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + }, + }) + ).toMatchObject({ + ANY_ADDITIONAL_PROPERTY: { + foo: 'text', + }, + }); + }); + + it('works with anyOf', () => { + expect( + generateSchemaExample({ + title: 'Foo', + type: 'object', + anyOf: [ + { + type: 'object', + required: ['a'], + properties: { + a: { + type: 'integer', + format: 'int32', + }, + }, + }, + { + type: 'object', + required: ['b'], + properties: { + b: { + type: 'string', + }, + }, + }, + ], + required: ['c'], + properties: { + c: { + type: 'boolean', + }, + }, + }) + ).toStrictEqual({ + a: 1, + c: true, + }); + }); + + it('deals with circular references', () => { + const schema = { + type: 'object', + properties: { + foobar: {}, + }, + } satisfies OpenAPIV3.SchemaObject; + + // Create a circular reference + schema.properties.foobar = schema; + + // 10 levels deep, that’s enough. It should return null then. + expect(generateSchemaExample(schema)).toStrictEqual({ + foobar: { + foobar: { + foobar: { + foobar: { + foobar: { + foobar: '[Circular Reference]', + }, + }, + }, + }, + }, + }); + }); + + it('handles patternProperties', () => { + expect( + generateSchemaExample({ + type: 'object', + patternProperties: { + '^(.*)$': { + type: 'object', + properties: { + dataId: { + type: 'string', + }, + link: { + anyOf: [ + { + format: 'uri', + type: 'string', + example: 'https://example.com', + }, + { + type: 'null', + }, + ], + }, + }, + required: ['dataId', 'link'], + }, + }, + }) + ).toStrictEqual({ + '^(.*)$': { + dataId: 'text', + link: 'https://example.com', + }, + }); + }); +}); diff --git a/packages/react-openapi/src/generateSchemaExample.ts b/packages/react-openapi/src/generateSchemaExample.ts index 01d06565c..5a038db06 100644 --- a/packages/react-openapi/src/generateSchemaExample.ts +++ b/packages/react-openapi/src/generateSchemaExample.ts @@ -16,14 +16,10 @@ export function generateSchemaExample( schema: OpenAPIV3.SchemaObject, options?: GenerateSchemaExampleOptions ): JSONValue | undefined { - return getExampleFromSchema( - schema, - { - emptyString: 'text', - ...options, - }, - 3 // Max depth for circular references - ); + return getExampleFromSchema(schema, { + emptyString: 'text', + ...options, + }); } /** @@ -103,21 +99,6 @@ function guessFromFormat(schema: Record, fallback = '') { return genericExampleValues[schema.format] ?? fallback; } -/** Map of all the results */ -const resultCache = new WeakMap, any>(); - -/** Store result in the cache, and return the result */ -function cache(schema: Record, result: unknown) { - // Avoid unnecessary WeakMap operations for primitive values - if (typeof result !== 'object' || result === null) { - return result; - } - - resultCache.set(schema, result); - - return result; -} - /** * This function takes an OpenAPI schema and generates an example from it * Forked from : https://github.com/scalar/scalar/blob/main/packages/oas-utils/src/spec-getters/getExampleFromSchema.ts @@ -152,8 +133,20 @@ const getExampleFromSchema = ( }, level = 0, parentSchema?: Record, - name?: string + name?: string, + resultCache = new WeakMap, any>() ): any => { + // Store result in the cache, and return the result + function cache(schema: Record, result: unknown) { + // Avoid unnecessary WeakMap operations for primitive values + if (typeof result !== 'object' || result === null) { + return result; + } + + resultCache.set(schema, result); + return result; + } + // Check if the result is already cached if (resultCache.has(schema)) { return resultCache.get(schema); @@ -173,6 +166,11 @@ const getExampleFromSchema = ( // But if `emptyString` is set, we do want to see some values. const makeUpRandomData = !!options?.emptyString; + // If the property is deprecated we don't show it in examples. + if (schema.deprecated) { + return undefined; + } + // Check if the property is read-only/write-only if ( (options?.mode === 'write' && schema.readOnly) || @@ -206,6 +204,14 @@ const getExampleFromSchema = ( return cache(schema, schema.example); } + // Use a default value, if there’s one and it’s a string or number + if ( + schema.default !== undefined && + ['string', 'number', 'boolean'].includes(typeof schema.default) + ) { + return cache(schema, schema.default); + } + // enum: [ 'available', 'pending', 'sold' ] if (Array.isArray(schema.enum) && schema.enum.length > 0) { return cache(schema, schema.enum[0]); @@ -245,7 +251,8 @@ const getExampleFromSchema = ( options, level + 1, schema, - propertyName + propertyName, + resultCache ); if (typeof response[propertyXmlTagName ?? propertyName] === 'undefined') { @@ -269,7 +276,8 @@ const getExampleFromSchema = ( options, level + 1, schema, - exampleKey + exampleKey, + resultCache ); } } @@ -290,21 +298,51 @@ const getExampleFromSchema = ( response.ANY_ADDITIONAL_PROPERTY = getExampleFromSchema( schema.additionalProperties, options, - level + 1 + level + 1, + undefined, + undefined, + resultCache ); } } if (schema.anyOf !== undefined) { - Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1)); + Object.assign( + response, + getExampleFromSchema( + schema.anyOf[0], + options, + level + 1, + undefined, + undefined, + resultCache + ) + ); } else if (schema.oneOf !== undefined) { - Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1)); + Object.assign( + response, + getExampleFromSchema( + schema.oneOf[0], + options, + level + 1, + undefined, + undefined, + resultCache + ) + ); } else if (schema.allOf !== undefined) { Object.assign( response, ...schema.allOf .map((item: Record) => - getExampleFromSchema(item, options, level + 1, schema) + getExampleFromSchema( + item, + options, + level + 1, + schema, + undefined, + resultCache + ) ) .filter((item: any) => item !== undefined) ); @@ -335,7 +373,9 @@ const getExampleFromSchema = ( { type: 'object', allOf: schema.items.allOf }, options, level + 1, - schema + schema, + undefined, + resultCache ); return cache( @@ -346,7 +386,14 @@ const getExampleFromSchema = ( // For non-objects (like strings), collect all examples const examples = schema.items.allOf .map((item: Record) => - getExampleFromSchema(item, options, level + 1, schema) + getExampleFromSchema( + item, + options, + level + 1, + schema, + undefined, + resultCache + ) ) .filter((item: any) => item !== undefined); @@ -368,7 +415,14 @@ const getExampleFromSchema = ( const schemas = schema.items[rule].slice(0, 1); const exampleFromRule = schemas .map((item: Record) => - getExampleFromSchema(item, options, level + 1, schema) + getExampleFromSchema( + item, + options, + level + 1, + schema, + undefined, + resultCache + ) ) .filter((item: any) => item !== undefined); @@ -380,7 +434,14 @@ const getExampleFromSchema = ( } if (schema.items?.type) { - const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1); + const exampleFromSchema = getExampleFromSchema( + schema.items, + options, + level + 1, + undefined, + undefined, + resultCache + ); return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema]; } @@ -407,7 +468,14 @@ const getExampleFromSchema = ( const firstOneOfItem = discriminateSchema[0]; // Return an example for the first item - return getExampleFromSchema(firstOneOfItem, options, level + 1); + return getExampleFromSchema( + firstOneOfItem, + options, + level + 1, + undefined, + undefined, + resultCache + ); } // Check if schema has the `allOf` key @@ -417,7 +485,14 @@ const getExampleFromSchema = ( // Loop through all `allOf` schemas schema.allOf.forEach((allOfItem: Record) => { // Return an example from the schema - const newExample = getExampleFromSchema(allOfItem, options, level + 1); + const newExample = getExampleFromSchema( + allOfItem, + options, + level + 1, + undefined, + undefined, + resultCache + ); // Merge or overwrite the example example = diff --git a/packages/react-openapi/src/getDisclosureLabel.ts b/packages/react-openapi/src/getDisclosureLabel.ts new file mode 100644 index 000000000..814113b91 --- /dev/null +++ b/packages/react-openapi/src/getDisclosureLabel.ts @@ -0,0 +1,25 @@ +'use client'; + +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import type { OpenAPIClientContext } from './context'; +import { tString } from './translate'; + +export function getDisclosureLabel(props: { + schema: OpenAPIV3.SchemaObject; + isExpanded: boolean; + context: OpenAPIClientContext; +}) { + const { schema, isExpanded, context } = props; + let label: string; + if (schema.type === 'array' && !!schema.items) { + if (schema.items.oneOf) { + label = tString(context.translation, 'available_items').toLowerCase(); + } else { + label = tString(context.translation, 'properties').toLowerCase(); + } + } else { + label = tString(context.translation, 'properties').toLowerCase(); + } + + return tString(context.translation, isExpanded ? 'hide' : 'show', label); +} diff --git a/packages/react-openapi/src/getOrCreateStoreByKey.ts b/packages/react-openapi/src/getOrCreateStoreByKey.ts new file mode 100644 index 000000000..c8f0f85ce --- /dev/null +++ b/packages/react-openapi/src/getOrCreateStoreByKey.ts @@ -0,0 +1,33 @@ +import { createStore } from 'zustand'; + +type Key = string | number; + +type State = { + key: Key | null; +}; + +type Actions = { setKey: (key: Key | null) => void }; + +export type Store = State & Actions; + +const createStateStore = (initial?: Key) => { + return createStore()((set) => ({ + key: initial ?? null, + setKey: (key) => { + set(() => ({ key })); + }, + })); +}; + +const defaultStores = new Map>(); + +const createStateStoreFactory = (stores: typeof defaultStores) => { + return (storeKey: string, initialKey?: Key) => { + if (!stores.has(storeKey)) { + stores.set(storeKey, createStateStore(initialKey)); + } + return stores.get(storeKey)!; + }; +}; + +export const getOrCreateStoreByKey = createStateStoreFactory(defaultStores); diff --git a/packages/react-openapi/src/index.ts b/packages/react-openapi/src/index.ts index f2e6f90bd..f097eab6a 100644 --- a/packages/react-openapi/src/index.ts +++ b/packages/react-openapi/src/index.ts @@ -1,5 +1,9 @@ export * from './schemas'; export * from './OpenAPIOperation'; +export * from './OpenAPIWebhook'; export * from './OpenAPIOperationContext'; export * from './resolveOpenAPIOperation'; -export type { OpenAPISchemasData, OpenAPIOperationData } from './types'; +export * from './resolveOpenAPIWebhook'; +export type { OpenAPIOperationData, OpenAPIWebhookData } from './types'; +export type { OpenAPIContextInput } from './context'; +export { checkIsValidLocale } from './translations'; diff --git a/packages/react-openapi/src/resolveOpenAPIOperation.ts b/packages/react-openapi/src/resolveOpenAPIOperation.ts index 295f2b79c..ba65b8bd6 100644 --- a/packages/react-openapi/src/resolveOpenAPIOperation.ts +++ b/packages/react-openapi/src/resolveOpenAPIOperation.ts @@ -40,16 +40,27 @@ export async function resolveOpenAPIOperation( } const servers = 'servers' in schema ? (schema.servers ?? []) : []; - const security = flattenSecurities(operation.security ?? schema.security ?? []); + const security: OpenAPIV3_1.SecurityRequirementObject[] = + operation.security ?? schema.security ?? []; + + // If security includes an empty object, it means that the security is optional + const isOptionalSecurity = security.some((entry) => Object.keys(entry).length === 0); + const flatSecurities = flattenSecurities(security); // Resolve securities const securities: OpenAPIOperationData['securities'] = []; - for (const entry of security) { + for (const entry of flatSecurities) { const securityKey = Object.keys(entry)[0]; if (securityKey) { const securityScheme = schema.components?.securitySchemes?.[securityKey]; if (securityScheme && !checkIsReference(securityScheme)) { - securities.push([securityKey, securityScheme]); + securities.push([ + securityKey, + { + ...securityScheme, + required: !isOptionalSecurity, + }, + ]); } } } diff --git a/packages/react-openapi/src/resolveOpenAPIWebhook.ts b/packages/react-openapi/src/resolveOpenAPIWebhook.ts new file mode 100644 index 000000000..f4c08a296 --- /dev/null +++ b/packages/react-openapi/src/resolveOpenAPIWebhook.ts @@ -0,0 +1,99 @@ +import { fromJSON, toJSON } from 'flatted'; + +import type { + Filesystem, + OpenAPIV3, + OpenAPIV3_1, + OpenAPIV3xDocument, +} from '@gitbook/openapi-parser'; +import { dereferenceFilesystem } from './dereference'; +import type { OpenAPIWebhookData } from './types'; + +export { fromJSON, toJSON }; + +/** + * Resolve an OpenAPI webhook in a file and compile it to a more usable format. + */ +export async function resolveOpenAPIWebhook( + filesystem: Filesystem, + webhookDescriptor: { + name: string; + method: string; + } +): Promise { + const { name, method } = webhookDescriptor; + const schema = await dereferenceFilesystem(filesystem); + let operation = getWebhookByNameAndMethod(schema, name, method); + + if (!operation) { + return null; + } + + // Resolve common parameters + const commonParameters = getPathObjectParameter(schema, name); + if (commonParameters) { + operation = { + ...operation, + parameters: [...commonParameters, ...(operation.parameters ?? [])], + }; + } + + const servers = 'servers' in schema ? (schema.servers ?? []) : []; + + return { + servers, + operation, + method, + name, + }; +} + +/** + * Get a path object from its path. + */ +function getPathObject( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + name: string +): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null { + if (schema.webhooks?.[name]) { + return schema.webhooks[name]; + } + return null; +} + +/** + * Resolve parameters from a path in an OpenAPI schema. + */ +function getPathObjectParameter( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + path: string +): + | (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] + | (OpenAPIV3.ParameterObject | OpenAPIV3_1.ReferenceObject)[] + | null { + const pathObject = getPathObject(schema, path); + if (pathObject?.parameters) { + return pathObject.parameters; + } + return null; +} + +/** + * Get an operation by its path and method. + */ +function getWebhookByNameAndMethod( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + name: string, + method: string +): OpenAPIV3.OperationObject | null { + // Types are buffy for OpenAPIV3_1.OperationObject, so we use v3 + const pathObject = getPathObject(schema, name); + if (!pathObject) { + return null; + } + const normalizedMethod = method.toLowerCase(); + if (!pathObject[normalizedMethod]) { + return null; + } + return pathObject[normalizedMethod]; +} diff --git a/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx new file mode 100644 index 000000000..86b7d49e8 --- /dev/null +++ b/packages/react-openapi/src/schemas/OpenAPISchemaItem.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { SectionBody } from '../StaticSection'; + +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIDisclosure } from '../OpenAPIDisclosure'; +import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; +import { Section } from '../StaticSection'; +import type { OpenAPIClientContext } from '../context'; +import { getDisclosureLabel } from '../getDisclosureLabel'; + +export function OpenAPISchemaItem(props: { + name: string; + schema: OpenAPIV3.SchemaObject; + context: OpenAPIClientContext; +}) { + const { schema, context, name } = props; + + return ( + getDisclosureLabel({ schema, isExpanded, context })} + > +
+ + + +
+
+ ); +} diff --git a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx index 7f024151e..c0700a530 100644 --- a/packages/react-openapi/src/schemas/OpenAPISchemas.tsx +++ b/packages/react-openapi/src/schemas/OpenAPISchemas.tsx @@ -1,99 +1,98 @@ +import type { OpenAPISchema } from '@gitbook/openapi-parser'; import clsx from 'clsx'; -import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup'; +import { OpenAPIExample } from '../OpenAPIExample'; import { OpenAPIRootSchema } from '../OpenAPISchemaServer'; -import { Section, SectionBody } from '../StaticSection'; -import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPISchemasData } from '../types'; - -type OpenAPISchemasContextProps = Omit< - OpenAPIContextProps, - 'renderCodeBlock' | 'renderHeading' | 'renderDocument' ->; +import { StaticSection } from '../StaticSection'; +import { + type OpenAPIContextInput, + getOpenAPIClientContext, + resolveOpenAPIContext, +} from '../context'; +import { t } from '../translate'; +import { getExampleFromSchema } from '../util/example'; +import { OpenAPISchemaItem } from './OpenAPISchemaItem'; /** - * Display OpenAPI Schemas. + * OpenAPI Schemas component. */ export function OpenAPISchemas(props: { className?: string; - data: OpenAPISchemasData; - context: OpenAPISchemasContextProps; + schemas: OpenAPISchema[]; + context: OpenAPIContextInput; /** * Whether to show the schema directly if there is only one. */ grouped?: boolean; }) { - const { className, data, context, grouped } = props; - const { schemas } = data; + const { schemas, context: contextInput, grouped, className } = props; - const clientContext: OpenAPIClientContext = { - defaultInteractiveOpened: context.defaultInteractiveOpened, - icons: context.icons, - blockKey: context.blockKey, - }; + const firstSchema = schemas[0]; - if (!schemas.length) { + if (!firstSchema) { return null; } - return ( -
- -
- ); -} - -/** - * Root schema for OpenAPI schemas. - * It displays a single model or a disclosure group for multiple schemas. - */ -function OpenAPIRootSchemasSchema(props: { - schemas: OpenAPISchemasData['schemas']; - context: OpenAPIClientContext; - grouped?: boolean; -}) { - const { schemas, context, grouped } = props; + const context = resolveOpenAPIContext(contextInput); + const clientContext = getOpenAPIClientContext(context); // If there is only one model and we are not grouping, we show it directly. if (schemas.length === 1 && !grouped) { - const schema = schemas?.[0]?.schema; - - if (!schema) { - return null; - } - + const title = `The ${firstSchema.name} object`; return ( -
- - - -
+
+
+ {context.renderHeading({ + title, + deprecated: Boolean(firstSchema.schema.deprecated), + stability: firstSchema.schema['x-stability'], + })} +
+
+
+ + + +
+
+
+
+

{title}

+
+ +
+
+
+
+
+
); } // If there are multiple schemas, we use a disclosure group to show them all. return ( - ({ - id: name, - label: ( -
- {name} -
- ), - tabs: [ - { - id: 'model', - body: ( -
- - - -
- ), - }, - ], - }))} - /> +
+ {schemas.map(({ name, schema }) => { + return ( + + ); + })} +
); } diff --git a/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts b/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts index 2e03c1de5..5497468f1 100644 --- a/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts +++ b/packages/react-openapi/src/schemas/resolveOpenAPISchemas.ts @@ -1,9 +1,6 @@ -import type { Filesystem, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; +import type { Filesystem, OpenAPISchema, OpenAPIV3xDocument } from '@gitbook/openapi-parser'; import { filterSelectedOpenAPISchemas } from '@gitbook/openapi-parser'; import { dereferenceFilesystem } from '../dereference'; -import type { OpenAPISchemasData } from '../types'; - -//!!TODO: We should return only the schemas that are used in the block. Still a WIP awaiting future work. /** * Resolve an OpenAPI schemas from a file and compile it to a more usable format. @@ -14,7 +11,9 @@ export async function resolveOpenAPISchemas( options: { schemas: string[]; } -): Promise { +): Promise<{ + schemas: OpenAPISchema[]; +} | null> { const { schemas: selectedSchemas } = options; const schema = await dereferenceFilesystem(filesystem); diff --git a/packages/react-openapi/src/stringifyOpenAPI.ts b/packages/react-openapi/src/stringifyOpenAPI.ts index c0d070c2d..3fb2853de 100644 --- a/packages/react-openapi/src/stringifyOpenAPI.ts +++ b/packages/react-openapi/src/stringifyOpenAPI.ts @@ -1,17 +1,25 @@ /** * Stringify an OpenAPI object. Same API as JSON.stringify. */ -export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string { +export function stringifyOpenAPI( + value: any, + replacer?: ((this: any, key: string, value: any) => any) | null, + space?: string | number +): string { return JSON.stringify( - body, + value, (key, value) => { // Ignore internal keys if (key.startsWith('x-gitbook-')) { return undefined; } + if (replacer) { + return replacer(key, value); + } + return value; }, - indent + space ); } diff --git a/packages/react-openapi/src/translate.tsx b/packages/react-openapi/src/translate.tsx new file mode 100644 index 000000000..f1dba8f23 --- /dev/null +++ b/packages/react-openapi/src/translate.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import type { Translation, TranslationKey } from './translations'; + +/** + * Translate a string. + */ +export function t( + translation: Translation, + id: TranslationKey, + ...args: React.ReactNode[] +): React.ReactNode { + const string = translation[id]; + if (!string) { + throw new Error(`Translation not found for "${id}"`); + } + + // Now we are going to replace the arguments + // but we want to return a string as long as it's possible + // (eg. if there isn't any argument that is a ReactNode) + const parts: React.ReactNode[] = []; + let currentStringToReplace: string = string; + + args.forEach((arg, i) => { + if (typeof arg === 'string') { + currentStringToReplace = currentStringToReplace.replace(`\${${i + 1}}`, arg); + } else { + const [partToPush, partToReplace] = currentStringToReplace.split(`\${${i + 1}}`); + if (partToPush === undefined || partToReplace === undefined) { + throw new Error(`Invalid translation "${id}"`); + } + parts.push({partToPush}); + parts.push({arg}); + currentStringToReplace = partToReplace; + } + }); + + if (!parts.length) { + return currentStringToReplace; + } + + return ( + <> + {parts} + {currentStringToReplace} + + ); +} + +/** + * Version of `t` that returns a string. + */ +export function tString( + translation: Translation, + id: TranslationKey, + ...args: React.ReactNode[] +): string { + const result = t(translation, id, ...args); + return reactToString(result); +} + +function reactToString(el: React.ReactNode): string { + if (typeof el === 'string' || typeof el === 'number' || typeof el === 'boolean') { + return `${el}`; + } + + if (el === null || el === undefined) { + return ''; + } + + if (Array.isArray(el)) { + return el.map(reactToString).join(''); + } + + if (typeof el === 'object' && 'props' in el) { + return el.props.children.map(reactToString).join(''); + } + + throw new Error(`Unsupported type ${typeof el}`); +} diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts new file mode 100644 index 000000000..8a5236c58 --- /dev/null +++ b/packages/react-openapi/src/translations/de.ts @@ -0,0 +1,42 @@ +export const de = { + required: 'Erforderlich', + deprecated: 'Veraltet', + deprecated_and_sunset_on: 'Diese Operation ist veraltet und wird am ${1} eingestellt.', + stability_experimental: 'Experimentell', + stability_alpha: 'Alpha', + stability_beta: 'Beta', + copy_to_clipboard: 'In die Zwischenablage kopieren', + copied: 'Kopiert', + no_content: 'Kein Inhalt', + unresolved_reference: 'Nicht aufgelöste Referenz', + circular_reference: 'Zirkuläre Referenz', + read_only: 'Nur lesen', + write_only: 'Nur schreiben', + optional: 'Optional', + min: 'Min', + max: 'Max', + nullable: 'Nullfähig', + body: 'Rumpf', + payload: 'Nutzlast', + headers: 'Header', + header: 'Header', + authorizations: 'Autorisierungen', + responses: 'Antworten', + response: 'Antwort', + path_parameters: 'Pfadparameter', + query_parameters: 'Abfrageparameter', + header_parameters: 'Header-Parameter', + attributes: 'Attribute', + test_it: 'Teste es', + information: 'Information', + success: 'Erfolg', + redirect: 'Umleitung', + error: 'Fehler', + show: 'Zeige ${1}', + hide: 'Verstecke ${1}', + available_items: 'Verfügbare Elemente', + properties: 'Eigenschaften', + or: 'oder', + and: 'und', + possible_values: 'Mögliche Werte', +}; diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts new file mode 100644 index 000000000..4276595d5 --- /dev/null +++ b/packages/react-openapi/src/translations/en.ts @@ -0,0 +1,42 @@ +export const en = { + required: 'Required', + deprecated: 'Deprecated', + deprecated_and_sunset_on: 'This operation is deprecated and will be sunset on ${1}.', + stability_experimental: 'Experimental', + stability_alpha: 'Alpha', + stability_beta: 'Beta', + copy_to_clipboard: 'Copy to clipboard', + copied: 'Copied', + no_content: 'No content', + unresolved_reference: 'Unresolved reference', + circular_reference: 'Circular reference', + read_only: 'Read-only', + write_only: 'Write-only', + optional: 'Optional', + min: 'Min', + max: 'Max', + nullable: 'Nullable', + body: 'Body', + payload: 'Payload', + headers: 'Headers', + header: 'Header', + authorizations: 'Authorizations', + responses: 'Responses', + response: 'Response', + path_parameters: 'Path parameters', + query_parameters: 'Query parameters', + header_parameters: 'Header parameters', + attributes: 'Attributes', + test_it: 'Test it', + information: 'Information', + success: 'Success', + redirect: 'Redirect', + error: 'Error', + show: 'Show ${1}', + hide: 'Hide ${1}', + available_items: 'Available items', + possible_values: 'Possible values', + properties: 'Properties', + or: 'or', + and: 'and', +}; diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts new file mode 100644 index 000000000..f5ac905f6 --- /dev/null +++ b/packages/react-openapi/src/translations/es.ts @@ -0,0 +1,42 @@ +export const es = { + required: 'Requerido', + deprecated: 'Obsoleto', + deprecated_and_sunset_on: 'Esta operación está obsoleta y se retirará el ${1}.', + stability_experimental: 'Experimental', + stability_alpha: 'Alfa', + stability_beta: 'Beta', + copy_to_clipboard: 'Copiar al portapapeles', + copied: 'Copiado', + no_content: 'Sin contenido', + unresolved_reference: 'Referencia no resuelta', + circular_reference: 'Referencia circular', + read_only: 'Solo lectura', + write_only: 'Solo escritura', + optional: 'Opcional', + min: 'Mín', + max: 'Máx', + nullable: 'Nulo', + body: 'Cuerpo', + payload: 'Caga útil', + headers: 'Headers', + header: 'Header', + authorizations: 'Autorizaciones', + responses: 'Respuestas', + response: 'Respuesta', + path_parameters: 'Parámetros de ruta', + query_parameters: 'Parámetros de consulta', + header_parameters: 'Parámetros de encabezado', + attributes: 'Atributos', + test_it: 'Pruébalo', + information: 'Información', + success: 'Éxito', + redirect: 'Redirección', + error: 'Error', + show: 'Mostrar ${1}', + hide: 'Ocultar ${1}', + available_items: 'Elementos disponibles', + properties: 'Propiedades', + or: 'o', + and: 'y', + possible_values: 'Valores posibles', +}; diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts new file mode 100644 index 000000000..b2e563c81 --- /dev/null +++ b/packages/react-openapi/src/translations/fr.ts @@ -0,0 +1,42 @@ +export const fr = { + required: 'Requis', + deprecated: 'Obsolète', + deprecated_and_sunset_on: 'Cette opération est obsolète et sera supprimée le ${1}.', + stability_experimental: 'Expérimental', + stability_alpha: 'Alpha', + stability_beta: 'Bêta', + copy_to_clipboard: 'Copier dans le presse-papiers', + copied: 'Copié', + no_content: 'Aucun contenu', + unresolved_reference: 'Référence non résolue', + circular_reference: 'Référence circulaire', + read_only: 'Lecture seule', + write_only: 'Écriture seule', + optional: 'Optionnel', + min: 'Min', + max: 'Max', + nullable: 'Nullable', + body: 'Corps', + payload: 'Charge utile', + headers: 'Headers', + header: 'Header', + authorizations: 'Autorisations', + responses: 'Réponses', + response: 'Réponse', + path_parameters: 'Paramètres de chemin', + query_parameters: 'Paramètres de requête', + header_parameters: "Paramètres d'en-tête", + attributes: 'Attributs', + test_it: 'Tester', + information: 'Information', + success: 'Succès', + redirect: 'Redirection', + error: 'Erreur', + show: 'Afficher ${1}', + hide: 'Masquer ${1}', + available_items: 'Éléments disponibles', + properties: 'Propriétés', + or: 'ou', + and: 'et', + possible_values: 'Valeurs possibles', +}; diff --git a/packages/react-openapi/src/translations/index.ts b/packages/react-openapi/src/translations/index.ts new file mode 100644 index 000000000..690b33187 --- /dev/null +++ b/packages/react-openapi/src/translations/index.ts @@ -0,0 +1,33 @@ +import { de } from './de'; +import { en } from './en'; +import { es } from './es'; +import { fr } from './fr'; +import { ja } from './ja'; +import { nl } from './nl'; +import { no } from './no'; +import { pt_br } from './pt-br'; +import type { Translation } from './types'; +import { zh } from './zh'; + +export * from './types'; + +export const translations = { + en, + de, + es, + fr, + ja, + nl, + no, + 'pt-br': pt_br, + zh, +} satisfies Record; + +export type TranslationLocale = keyof typeof translations; + +/** + * Check if the locale is valid. + */ +export function checkIsValidLocale(locale: string): locale is TranslationLocale { + return Object.prototype.hasOwnProperty.call(translations, locale); +} diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts new file mode 100644 index 000000000..e393dd6cb --- /dev/null +++ b/packages/react-openapi/src/translations/ja.ts @@ -0,0 +1,42 @@ +export const ja = { + required: '必須', + deprecated: '非推奨', + deprecated_and_sunset_on: 'この操作は非推奨であり、${1}に廃止されます。', + stability_experimental: '実験的', + stability_alpha: 'アルファ', + stability_beta: 'ベータ', + copy_to_clipboard: 'クリップボードにコピー', + copied: 'コピー済み', + no_content: 'コンテンツなし', + unresolved_reference: '未解決の参照', + circular_reference: '循環参照', + read_only: '読み取り専用', + write_only: '書き込み専用', + optional: 'オプション', + min: '最小', + max: '最大', + nullable: 'ヌル許容', + body: '本文', + payload: 'ペイロード', + headers: 'ヘッダー', + header: 'ヘッダー', + authorizations: '認可', + responses: 'レスポンス', + response: 'レスポンス', + path_parameters: 'パスパラメータ', + query_parameters: 'クエリパラメータ', + header_parameters: 'ヘッダーパラメータ', + attributes: '属性', + test_it: 'テストする', + information: '情報', + success: '成功', + redirect: 'リダイレクト', + error: 'エラー', + show: '${1}を表示', + hide: '${1}を非表示', + available_items: '利用可能なアイテム', + properties: 'プロパティ', + or: 'または', + and: 'かつ', + possible_values: '可能な値', +}; diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts new file mode 100644 index 000000000..34867ccf1 --- /dev/null +++ b/packages/react-openapi/src/translations/nl.ts @@ -0,0 +1,42 @@ +export const nl = { + required: 'Vereist', + deprecated: 'Verouderd', + deprecated_and_sunset_on: 'Deze bewerking is verouderd en wordt op ${1} beëindigd.', + stability_experimental: 'Experimenteel', + stability_alpha: 'Alfa', + stability_beta: 'Bèta', + copy_to_clipboard: 'Kopiëren naar klembord', + copied: 'Gekopieerd', + no_content: 'Geen inhoud', + unresolved_reference: 'Onopgeloste verwijzing', + circular_reference: 'Circulaire verwijzing', + read_only: 'Alleen lezen', + write_only: 'Alleen schrijven', + optional: 'Optioneel', + min: 'Min', + max: 'Max', + nullable: 'Null toegestaan', + body: 'Body', + payload: 'Payload', + headers: 'Headers', + header: 'Header', + authorizations: 'Autorisaties', + responses: 'Reacties', + response: 'Reactie', + path_parameters: 'Padparameters', + query_parameters: 'Queryparameters', + header_parameters: 'Headerparameters', + attributes: 'Attributen', + test_it: 'Test het', + information: 'Informatie', + success: 'Succes', + redirect: 'Omleiding', + error: 'Fout', + show: 'Toon ${1}', + hide: 'Verberg ${1}', + available_items: 'Beschikbare items', + properties: 'Eigenschappen', + or: 'of', + and: 'en', + possible_values: 'Mogelijke waarden', +}; diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts new file mode 100644 index 000000000..270117793 --- /dev/null +++ b/packages/react-openapi/src/translations/no.ts @@ -0,0 +1,42 @@ +export const no = { + required: 'Påkrevd', + deprecated: 'Foreldet', + deprecated_and_sunset_on: 'Denne operasjonen er foreldet og vil bli avviklet den ${1}.', + stability_experimental: 'Eksperimentell', + stability_alpha: 'Alfa', + stability_beta: 'Beta', + copy_to_clipboard: 'Kopier til utklippstavle', + copied: 'Kopiert', + no_content: 'Ingen innhold', + unresolved_reference: 'Uavklart referanse', + circular_reference: 'Sirkulær referanse', + read_only: 'Skrivebeskyttet', + write_only: 'Kun skriving', + optional: 'Valgfri', + min: 'Min', + max: 'Maks', + nullable: 'Kan være null', + body: 'Brødtekst', + payload: 'Nyttelast', + headers: 'Headers', + header: 'Header', + authorizations: 'Autorisasjoner', + responses: 'Responser', + response: 'Respons', + path_parameters: 'Sti-parametere', + query_parameters: 'Forespørselsparametere', + header_parameters: 'Header-parametere', + attributes: 'Attributter', + test_it: 'Test det', + information: 'Informasjon', + success: 'Suksess', + redirect: 'Viderekobling', + error: 'Feil', + show: 'Vis ${1}', + hide: 'Skjul ${1}', + available_items: 'Tilgjengelige elementer', + properties: 'Egenskaper', + or: 'eller', + and: 'og', + possible_values: 'Mulige verdier', +}; diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts new file mode 100644 index 000000000..00e8ab3c2 --- /dev/null +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -0,0 +1,42 @@ +export const pt_br = { + required: 'Obrigatório', + deprecated: 'Obsoleto', + deprecated_and_sunset_on: 'Esta operação está obsoleta e será descontinuada em ${1}.', + stability_experimental: 'Experimental', + stability_alpha: 'Alfa', + stability_beta: 'Beta', + copy_to_clipboard: 'Copiar para a área de transferência', + copied: 'Copiado', + no_content: 'Sem conteúdo', + unresolved_reference: 'Referência não resolvida', + circular_reference: 'Referência circular', + read_only: 'Somente leitura', + write_only: 'Somente escrita', + optional: 'Opcional', + min: 'Mín', + max: 'Máx', + nullable: 'Nulo', + body: 'Corpo', + payload: 'Carga útil', + headers: 'Headers', + header: 'Header', + authorizations: 'Autorizações', + responses: 'Respostas', + response: 'Resposta', + path_parameters: 'Parâmetros de rota', + query_parameters: 'Parâmetros de consulta', + header_parameters: 'Parâmetros de cabeçalho', + attributes: 'Atributos', + test_it: 'Testar', + information: 'Informação', + success: 'Sucesso', + redirect: 'Redirecionamento', + error: 'Erro', + show: 'Mostrar ${1}', + hide: 'Ocultar ${1}', + available_items: 'Itens disponíveis', + properties: 'Propriedades', + or: 'ou', + and: 'e', + possible_values: 'Valores possíveis', +}; diff --git a/packages/react-openapi/src/translations/types.ts b/packages/react-openapi/src/translations/types.ts new file mode 100644 index 000000000..06e131d06 --- /dev/null +++ b/packages/react-openapi/src/translations/types.ts @@ -0,0 +1,7 @@ +import type { en } from './en'; + +export type TranslationKey = keyof typeof en; + +export type Translation = { + [key in TranslationKey]: string; +}; diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts new file mode 100644 index 000000000..f8299d199 --- /dev/null +++ b/packages/react-openapi/src/translations/zh.ts @@ -0,0 +1,42 @@ +export const zh = { + required: '必填', + deprecated: '已弃用', + deprecated_and_sunset_on: '此操作已弃用,将于 ${1} 停止使用。', + stability_experimental: '实验性', + stability_alpha: 'Alpha', + stability_beta: 'Beta', + copy_to_clipboard: '复制到剪贴板', + copied: '已复制', + no_content: '无内容', + unresolved_reference: '未解析的引用', + circular_reference: '循环引用', + read_only: '只读', + write_only: '只写', + optional: '可选', + min: '最小值', + max: '最大值', + nullable: '可为 null', + body: '请求体', + payload: '有效载荷', + headers: '头字段', + header: '头部', + authorizations: '授权', + responses: '响应', + response: '响应', + path_parameters: '路径参数', + query_parameters: '查询参数', + header_parameters: '头参数', + attributes: '属性', + test_it: '测试一下', + information: '信息', + success: '成功', + redirect: '重定向', + error: '错误', + show: '显示${1}', + hide: '隐藏${1}', + available_items: '可用项', + properties: '属性', + or: '或', + and: '和', + possible_values: '可能的值', +}; diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 011b420f1..625bdd18b 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -1,51 +1,10 @@ import type { OpenAPICustomOperationProperties, OpenAPICustomSpecProperties, - OpenAPISchema, OpenAPIV3, } from '@gitbook/openapi-parser'; -export interface OpenAPIContextProps extends OpenAPIClientContext { - /** - * Render a code block. - */ - renderCodeBlock: (props: { code: string; syntax: string }) => React.ReactNode; - /** - * Render the heading of the operation. - */ - renderHeading: (props: { - deprecated: boolean; - title: string; - stability?: string; - }) => React.ReactNode; - /** - * Render the document of the operation. - */ - renderDocument: (props: { document: object }) => React.ReactNode; - - /** Spec url for the Scalar Api Client */ - specUrl: string; -} - -export interface OpenAPIClientContext { - icons: { - chevronDown: React.ReactNode; - chevronRight: React.ReactNode; - plus: React.ReactNode; - }; - - /** - * Force all sections to be opened by default. - * @default false - */ - defaultInteractiveOpened?: boolean; - /** - * The key of the block - */ - blockKey?: string; - /** Optional id attached to the OpenAPI Operation heading and used as an anchor */ - id?: string; -} +export type OpenAPISecurityWithRequired = OpenAPIV3.SecuritySchemeObject & { required?: boolean }; export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { path: string; @@ -58,10 +17,16 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties { operation: OpenAPIV3.OperationObject; /** Securities that should be used for this operation */ - securities: [string, OpenAPIV3.SecuritySchemeObject][]; + securities: [string, OpenAPISecurityWithRequired][]; } -export interface OpenAPISchemasData { - /** Components schemas to be used for schemas */ - schemas: OpenAPISchema[]; +export interface OpenAPIWebhookData extends OpenAPICustomSpecProperties { + name: string; + method: string; + + /** Servers to be used for this operation */ + servers: OpenAPIV3.ServerObject[]; + + /** Spec of the webhook */ + operation: OpenAPIV3.OperationObject; } diff --git a/packages/react-openapi/src/useSyncedTabsGlobalState.ts b/packages/react-openapi/src/useSyncedTabsGlobalState.ts deleted file mode 100644 index 42a6c3a09..000000000 --- a/packages/react-openapi/src/useSyncedTabsGlobalState.ts +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import { createStore } from 'zustand'; - -type Key = string | number; - -type TabState = { - tabKey: Key | null; -}; - -type TabActions = { setTabKey: (tab: Key | null) => void }; - -type TabStore = TabState & TabActions; - -const createTabStore = (initialTab?: Key) => { - return createStore()((set) => ({ - tabKey: initialTab ?? null, - setTabKey: (tabKey) => { - set(() => ({ tabKey })); - }, - })); -}; - -const defaultTabStores = new Map>(); - -const createTabStoreFactory = (stores: typeof defaultTabStores) => { - return (storeKey: string, initialKey?: Key) => { - if (!stores.has(storeKey)) { - stores.set(storeKey, createTabStore(initialKey)); - } - return stores.get(storeKey)!; - }; -}; - -export const getOrCreateTabStoreByKey = createTabStoreFactory(defaultTabStores); diff --git a/packages/react-openapi/src/util/example.tsx b/packages/react-openapi/src/util/example.tsx new file mode 100644 index 000000000..95b295e58 --- /dev/null +++ b/packages/react-openapi/src/util/example.tsx @@ -0,0 +1,129 @@ +import type { OpenAPIV3 } from '@gitbook/openapi-parser'; +import { OpenAPIExample } from '../OpenAPIExample'; +import type { OpenAPIContext } from '../context'; +import { generateSchemaExample } from '../generateSchemaExample'; +import { tString } from '../translate'; +import { checkIsReference } from '../utils'; + +/** + * Generate an example from a reference object. + */ +export function getExampleFromReference( + ref: OpenAPIV3.ReferenceObject, + context: OpenAPIContext +): OpenAPIV3.ExampleObject { + return { + summary: tString(context.translation, 'unresolved_reference'), + value: { $ref: ref.$ref }, + }; +} + +/** + * Get examples from a media type object. + */ +export function getExamplesFromMediaTypeObject(args: { + mediaType: string; + mediaTypeObject: OpenAPIV3.MediaTypeObject; + context: OpenAPIContext; +}): { key: string; example: OpenAPIV3.ExampleObject }[] { + const { mediaTypeObject, mediaType, context } = args; + if (mediaTypeObject.examples) { + return Object.entries(mediaTypeObject.examples).map(([key, example]) => { + return { + key, + example: checkIsReference(example) + ? getExampleFromReference(example, context) + : example, + }; + }); + } + + if (mediaTypeObject.example) { + return [{ key: 'default', example: { value: mediaTypeObject.example } }]; + } + + if (mediaTypeObject.schema) { + if (mediaType === 'application/xml') { + // @TODO normally we should use the name of the schema but we don't have it + // fix it when we got the reference name + const root = mediaTypeObject.schema.xml?.name ?? 'object'; + return [ + { + key: 'default', + example: { + value: { + [root]: generateSchemaExample(mediaTypeObject.schema, { + xml: mediaType === 'application/xml', + mode: 'read', + }), + }, + }, + }, + ]; + } + return [ + { + key: 'default', + example: { + value: generateSchemaExample(mediaTypeObject.schema, { + mode: 'read', + }), + }, + }, + ]; + } + return []; +} + +/** + * Get example from a schema object. + */ +export function getExampleFromSchema(args: { + schema: OpenAPIV3.SchemaObject; +}): OpenAPIV3.ExampleObject { + const { schema } = args; + + if (schema.example) { + return { value: schema.example }; + } + + return { value: generateSchemaExample(schema, { mode: 'read' }) }; +} + +/** + * Get the examples from a media type object. + */ +export function getExamples(props: { + mediaTypeObject: OpenAPIV3.MediaTypeObject; + mediaType: string; + context: OpenAPIContext; +}) { + const { mediaTypeObject, mediaType, context } = props; + const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType, context }); + const syntax = getSyntaxFromMediaType(mediaType); + + return examples.map((example) => { + return { + key: example.key, + label: example.example.summary || example.key, + body: ( + + ), + }; + }); +} + +/** + * Get the syntax from a media type. + */ +function getSyntaxFromMediaType(mediaType: string): string { + if (mediaType.includes('json')) { + return 'json'; + } + + if (mediaType === 'application/xml') { + return 'xml'; + } + + return 'text'; +} diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index b882804fd..0ac9b78d5 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -1,5 +1,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'; +import type { OpenAPIUniversalContext } from './context'; import { stringifyOpenAPI } from './stringifyOpenAPI'; +import { tString } from './translate'; export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject { return typeof input === 'object' && !!input && '$ref' in input; @@ -149,3 +151,68 @@ function shouldDisplayExample(schema: OpenAPIV3.SchemaObject): boolean { Object.keys(schema.example).length > 0) ); } + +/** + * Get the class name for a status code. + * 1xx: informational + * 2xx: success + * 3xx: redirect + * 4xx, 5xx: error + */ +export function getStatusCodeClassName(statusCode: number | string): string { + const category = getStatusCodeCategory(statusCode); + switch (category) { + case 1: + return 'informational'; + case 2: + return 'success'; + case 3: + return 'redirect'; + case 4: + case 5: + return 'error'; + default: + return 'unknown'; + } +} + +/** + * Get a default label for a status code. + * This is used when there is no label provided in the OpenAPI spec. + * 1xx: Information + * 2xx: Success + * 3xx: Redirect + * 4xx, 5xx: Error + */ +export function getStatusCodeDefaultLabel( + statusCode: number | string, + context: OpenAPIUniversalContext +): string { + const category = getStatusCodeCategory(statusCode); + switch (category) { + case 1: + return tString(context.translation, 'information'); + case 2: + return tString(context.translation, 'success'); + case 3: + return tString(context.translation, 'redirect'); + case 4: + case 5: + return tString(context.translation, 'error'); + default: + return ''; + } +} + +function getStatusCodeCategory(statusCode: number | string): number | string { + const code = typeof statusCode === 'string' ? Number.parseInt(statusCode, 10) : statusCode; + + if (Number.isNaN(code) || code < 100 || code >= 600) { + return 'unknown'; + } + + // Determine the category of the status code based on the first digit + const category = Math.floor(code / 100); + + return category; +}