mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5356cd9e10 | |||
| 0af066bed8 | |||
| 26b2a6917d | |||
| 82cf22449f | |||
| 13eed34ff7 | |||
| 6733ac3fa0 | |||
| 497c5d325a | |||
| 264f4cc939 | |||
| 528d4a2475 | |||
| ad1e0e52af | |||
| d7b3aa97cd | |||
| 4acb8538f8 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show the theme toggle in the footer whenever the outline column that hosts the other toggle isn't pinned open, so it stays reachable on laptop-sized screens in wide layouts and while the AI chat is open.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Upgrade react-hotkeys-hook to v5 and use its native `useKey` option so keyboard shortcuts match the produced key on non-QWERTY layouts.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Load the OpenAPI, ContentKit, admin toolbar and PDF print stylesheets only on the pages that need them, cutting published pages from 8 render-blocking stylesheets to 3.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix the search field losing focus if it was focused just before the page finished hydrating.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Self-host the default fonts and inline only the families a site uses, instead of shipping the `@font-face` rules for all 23 supported fonts on every page.
|
||||
@@ -21,59 +21,59 @@ outputs:
|
||||
description: 'Deployment URL'
|
||||
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
using: 'composite'
|
||||
steps:
|
||||
- id: wrangler_status
|
||||
name: Check wrangler deployment status
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deployments status --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# This step is used to get the version ID that is currently deployed to Cloudflare.
|
||||
- id: extract_current_version
|
||||
- id: extract_current_version
|
||||
name: Extract current version
|
||||
shell: bash
|
||||
run: |
|
||||
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
|
||||
echo "version_id=$version_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- id: deploy_server
|
||||
name: Deploy server to Cloudflare at 0%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
|
||||
- id: deploy_middleware
|
||||
name: Deploy middleware to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
- name: Deploy server to Cloudflare at 100%
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
|
||||
@@ -67,19 +67,32 @@ runs:
|
||||
run: bun run turbo build:cloudflare
|
||||
env:
|
||||
GITBOOK_RUNTIME: cloudflare
|
||||
VERCEL_TARGET_ENV: ${{ inputs.environment }}
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: ${{ inputs.environment == 'preview' && 'true' || '' }}
|
||||
shell: bash
|
||||
|
||||
- id: extract_deployment_id
|
||||
name: Extract Next deployment ID
|
||||
shell: bash
|
||||
run: |
|
||||
deployment_id=$(bun -e '
|
||||
const buildOutput = await Bun.file("packages/gitbook/.open-next/server-functions/default/packages/gitbook/.next/required-server-files.json").json();
|
||||
const deploymentId = buildOutput.config?.deploymentId;
|
||||
if (!deploymentId) throw new Error("Next build did not emit a deployment ID");
|
||||
console.log(deploymentId);
|
||||
')
|
||||
echo "deployment_id=$deployment_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload the DO worker
|
||||
uses: cloudflare/wrangler-action@v3.14.0
|
||||
with:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: deploy --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc
|
||||
command: ${{ format('deploy --var OPEN_NEXT_BUILD_ID:{0} --config ./packages/gitbook/openNext/customWorkers/doWrangler.jsonc', steps.extract_deployment_id.outputs.deployment_id) }}
|
||||
|
||||
- id: upload_server
|
||||
name: Upload server to Cloudflare
|
||||
@@ -88,7 +101,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/defaultWrangler.jsonc
|
||||
|
||||
@@ -111,7 +124,7 @@ runs:
|
||||
apiToken: ${{ inputs.apiToken }}
|
||||
accountId: ${{ inputs.accountId }}
|
||||
workingDirectory: ./
|
||||
wranglerVersion: '4.43.0'
|
||||
wranglerVersion: '4.122.0'
|
||||
environment: ${{ inputs.environment }}
|
||||
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook/openNext/customWorkers/middlewareWrangler.jsonc
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"version": "0.27.2",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
"@cloudflare/workers-types": "^5.20260716.1",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
@@ -171,7 +171,7 @@
|
||||
"quick-lru": "^7.0.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hotkeys-hook": "^5.3.3",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
@@ -198,7 +198,6 @@
|
||||
"@argos-ci/playwright": "^7.2.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@scalar/api-client-react": "catalog:",
|
||||
"@tailwindcss/cli": "4.1.11",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
@@ -224,7 +223,7 @@
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.79.0",
|
||||
"wrangler": "^4.122.0",
|
||||
},
|
||||
},
|
||||
"packages/icons": {
|
||||
@@ -581,21 +580,21 @@
|
||||
|
||||
"@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="],
|
||||
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg=="],
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260401.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZSmceM70jH6k+/62VkEcmMNzrpr4kSctkX5Lsgqv38KktfhPY/hsh75y1lRoPWS3H3kgMa4p2pUSlidZR1u2hw=="],
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260811.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260401.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7UKWF+IUZ3NXMVPsDg8Cjg0r58b+uYlfvs5Yt8bvtU+geCtW4P2MxRHmRSEo8SryckXOJjb/b8tcncgCykFu8g=="],
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260811.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260401.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MDWUH/0bvL/l9aauN8zEddyYOXId1OueqrUCXXENNJ95R/lSmF6OgGVuXaYhoIhxQkNiEJ/0NOlnVYj9mJq4dw=="],
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260811.1", "", { "os": "linux", "cpu": "x64" }, "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260401.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UgkzpMzVWM/bwbo3vjCTg2aoKfGcUhiEoQoDdo6RGWvbHRJyLVZ4VQCG9ZcISiztkiS2ICCoYOtPy6M/lV6Gcw=="],
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260811.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260401.1", "", { "os": "win32", "cpu": "x64" }, "sha512-HBLzcQF5iF4Qv20tQ++pG7xs3OsCnaIbc+GAi6fmhUKZhvmzvml/jwrQzLJ+MPm0cQo41K5OO/U3T4S8tvJetQ=="],
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260811.1", "", { "os": "win32", "cpu": "x64" }, "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251011.0", "", {}, "sha512-gQpih+pbq3sP4uXltUeCSbPgZxTNp2gQd8639SaIbQMwgA6oJNHLhIART1fWy6DQACngiRzDVULA2x0ohmkGTQ=="],
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260716.1", "", {}, "sha512-LqQPmGAvdpQxzZGAMlDI6fnCsTlr8nRQibWsREaERqD0PucwFl25aXpUskSob70uSY2n6K1sp6Te9xkmzSFgaw=="],
|
||||
|
||||
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.4", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA=="],
|
||||
|
||||
@@ -1069,32 +1068,6 @@
|
||||
|
||||
"@panzoom/panzoom": ["@panzoom/panzoom@4.6.1", "", {}, "sha512-ogf/KhHHjj+DYAvHfaf3TXMQ8OE36pJtKpabLlx1OmpjcgtpCvkUoCiNONA8kuVRPGJdLiqMf0n8LRFXj1OyuA=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.6.0", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.4" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.6.0", "@parcel/watcher-darwin-arm64": "2.6.0", "@parcel/watcher-darwin-x64": "2.6.0", "@parcel/watcher-freebsd-x64": "2.6.0", "@parcel/watcher-linux-arm-glibc": "2.6.0", "@parcel/watcher-linux-arm-musl": "2.6.0", "@parcel/watcher-linux-arm64-glibc": "2.6.0", "@parcel/watcher-linux-arm64-musl": "2.6.0", "@parcel/watcher-linux-x64-glibc": "2.6.0", "@parcel/watcher-linux-x64-musl": "2.6.0", "@parcel/watcher-win32-arm64": "2.6.0", "@parcel/watcher-win32-x64": "2.6.0" } }, "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.6.0", "", { "os": "android", "cpu": "arm64" }, "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.6.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A=="],
|
||||
|
||||
"@phosphor-icons/core": ["@phosphor-icons/core@2.1.1", "", {}, "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
@@ -1343,8 +1316,6 @@
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
|
||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.1.11", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "enhanced-resolve": "^5.18.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.1.11" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-7RAFOrVaXCFz5ooEG36Kbh+sMJiI2j4+Ozp71smgjnLfBRu7DTfoq8DsTvzse2/6nDeo2M3vS/FGaxfDgr3rtQ=="],
|
||||
|
||||
"@tailwindcss/container-queries": ["@tailwindcss/container-queries@0.1.1", "", { "peerDependencies": { "tailwindcss": ">=3.2.0" } }, "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
@@ -1977,7 +1948,7 @@
|
||||
|
||||
"detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
@@ -2641,7 +2612,7 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20260401.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.4", "workerd": "1.20260401.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-lngHPzZFN9sxYG/mhzvnWiBMNVAN5MsO/7g32ttJ07rymtiK/ZBalODTKb8Od+BQdlU5DOR4CjVt9NydjnUyYg=="],
|
||||
"miniflare": ["miniflare@5.20260811.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260811.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
|
||||
|
||||
@@ -2683,8 +2654,6 @@
|
||||
|
||||
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="],
|
||||
@@ -2893,7 +2862,7 @@
|
||||
|
||||
"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@5.3.3", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ=="],
|
||||
"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=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
|
||||
@@ -3291,9 +3260,9 @@
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"workerd": ["workerd@1.20260401.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260401.1", "@cloudflare/workerd-darwin-arm64": "1.20260401.1", "@cloudflare/workerd-linux-64": "1.20260401.1", "@cloudflare/workerd-linux-arm64": "1.20260401.1", "@cloudflare/workerd-windows-64": "1.20260401.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-mUYCd+ohaWJWF5nhDzxugWaAD/DM8Dw0ze3B7bu8JaA7S70+XQJXcvcvwE8C4qGcxSdCyqjsrFzqxKubECDwzg=="],
|
||||
"workerd": ["workerd@1.20260811.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260811.1", "@cloudflare/workerd-darwin-arm64": "1.20260811.1", "@cloudflare/workerd-linux-64": "1.20260811.1", "@cloudflare/workerd-linux-arm64": "1.20260811.1", "@cloudflare/workerd-windows-64": "1.20260811.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw=="],
|
||||
|
||||
"wrangler": ["wrangler@4.80.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260401.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260401.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260401.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-2ZKF7uPeOZy65BGk3YfvqBCPo/xH1MrAlMmH9mVP+tCNBrTUMnwOHSj1HrZHgR8LttkAqhko0fGz+I4ax1rzyQ=="],
|
||||
"wrangler": ["wrangler@4.122.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260811.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260811.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260811.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-qkskzgQ76Y1qvVe5JARgvc3RISq6BC2rPoxQhFoKH1dKIwQc3GDFttQ/7m2OfeQ+tmQRzynv2dy/DXnxFCj2Lw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||
|
||||
@@ -3303,7 +3272,7 @@
|
||||
|
||||
"write-file-atomic": ["write-file-atomic@5.0.1", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"xdg-app-paths": ["xdg-app-paths@5.1.0", "", { "dependencies": { "xdg-portable": "^7.0.0" } }, "sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA=="],
|
||||
|
||||
@@ -3491,8 +3460,6 @@
|
||||
|
||||
"@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/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=="],
|
||||
|
||||
"@mapbox/node-pre-gyp/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
@@ -3511,8 +3478,6 @@
|
||||
|
||||
"@oxc-transform/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@parcel/watcher/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"@playwright/test/playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
|
||||
|
||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
@@ -3617,8 +3582,6 @@
|
||||
|
||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"@tailwindcss/oxide/tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||
@@ -3837,8 +3800,6 @@
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
|
||||
|
||||
"lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"lowlight/highlight.js": ["highlight.js@11.9.0", "", {}, "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw=="],
|
||||
|
||||
"mcp-handler/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
@@ -3869,7 +3830,9 @@
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"miniflare/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="],
|
||||
"miniflare/sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="],
|
||||
|
||||
"miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
|
||||
|
||||
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
@@ -3929,6 +3892,8 @@
|
||||
|
||||
"send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"side-channel-list/object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"side-channel-map/get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
@@ -4025,6 +3990,8 @@
|
||||
|
||||
"@argos-ci/core/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="],
|
||||
|
||||
"@argos-ci/core/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"@argos-ci/core/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="],
|
||||
@@ -4343,6 +4310,62 @@
|
||||
|
||||
"micro/raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"miniflare/sharp/@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.1" }, "os": "darwin", "cpu": "arm64" }, "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.1" }, "os": "darwin", "cpu": "x64" }, "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "os": "freebsd" }, "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.1", "", { "os": "linux", "cpu": "none" }, "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.1" }, "os": "linux", "cpu": "arm" }, "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.1" }, "os": "linux", "cpu": "ppc64" }, "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.1" }, "os": "linux", "cpu": "none" }, "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.1" }, "os": "linux", "cpu": "s390x" }, "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" }, "os": "linux", "cpu": "arm64" }, "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.1" }, "os": "linux", "cpu": "x64" }, "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.2", "", { "dependencies": { "@img/sharp-wasm32": "0.35.2" }, "cpu": "none" }, "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.2", "", { "os": "win32", "cpu": "x64" }, "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ=="],
|
||||
|
||||
"miniflare/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"miniflare/sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="],
|
||||
|
||||
"next/postcss/picocolors": ["picocolors@1.1.0", "", {}, "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="],
|
||||
@@ -4475,6 +4498,10 @@
|
||||
|
||||
"micro/raw-body/http-errors/toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.2", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
@@ -4509,6 +4536,10 @@
|
||||
|
||||
"@zenuml/core/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-freebsd-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-webcontainers-wasm32/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@opennextjs/aws/express/body-parser/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
@@ -21,11 +21,7 @@ const allowedDevOrigins =
|
||||
]
|
||||
: undefined;
|
||||
|
||||
// We don't use the deployment ID yet on 2c, we need to remove it because of https://github.com/opennextjs/opennextjs-aws/issues/1136
|
||||
let deploymentId =
|
||||
process.env.GITBOOK_RUNTIME === 'cloudflare'
|
||||
? undefined
|
||||
: process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
let deploymentId = process.env.GITBOOK_HEAD_SHA || process.env.GITHUB_SHA || Date.now().toString(); // Needed because we use a custom deployment method i.e. https://vercel.com/docs/skew-protection#custom-deployment-id
|
||||
|
||||
const { VERCEL_TARGET_ENV } = process.env;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ export default {
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/server').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
@@ -20,8 +20,8 @@ export default {
|
||||
proxyExternalRequest: 'fetch',
|
||||
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
|
||||
incrementalCache: () =>
|
||||
import('./openNext/incrementalCache/middleware').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
|
||||
import('./openNext/incrementalCache/cacheWorkerClient').then((m) => m.default),
|
||||
tagCache: () => import('./openNext/tagCache/cacheWorkerClient').then((m) => m.default),
|
||||
},
|
||||
},
|
||||
dangerous: {
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -70,6 +74,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -106,6 +114,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -150,6 +162,10 @@
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// This worker only purposes it to host the different DO that we will need in the other workers.
|
||||
import { DurableObject } from 'cloudflare:workers';
|
||||
|
||||
// `use cache` could cause multiple writes to the same key to happen concurrently, there is a limit of 1 write per key/second
|
||||
// so we need to buffer writes to the R2 bucket to avoid hitting this limit.
|
||||
export class R2WriteBuffer extends DurableObject {
|
||||
writePromise;
|
||||
|
||||
async write(cacheKey, value) {
|
||||
// We are already writing to this key
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.ctx.waitUntil(
|
||||
this.writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default {
|
||||
async fetch() {
|
||||
// This worker does not handle any requests, it only provides Durable Objects
|
||||
return new Response('This worker is not meant to handle requests directly', {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const runWithCloudflareRequestContext = mock(
|
||||
async <T>(_: Request, __: unknown, ___: unknown, operation: () => Promise<T>) => operation()
|
||||
);
|
||||
const get = mock();
|
||||
const getTagsFromValue = mock();
|
||||
const hasBeenRevalidated = mock();
|
||||
|
||||
mock.module('cloudflare:workers', () => ({
|
||||
DurableObject: class {},
|
||||
WorkerEntrypoint: class {},
|
||||
}));
|
||||
mock.module('../../.open-next/cloudflare/init.js', () => ({ runWithCloudflareRequestContext }));
|
||||
mock.module('../incrementalCache/incrementalCache', () => ({
|
||||
GitbookIncrementalCache: class {
|
||||
get = get;
|
||||
},
|
||||
}));
|
||||
mock.module('@opennextjs/aws/utils/cache.js', () => ({ getTagsFromValue }));
|
||||
mock.module('../tagCache/middleware', () => ({
|
||||
default: { hasBeenRevalidated },
|
||||
}));
|
||||
|
||||
const { default: IncrementalCacheWorker } = await import('./do');
|
||||
|
||||
const CACHE_CONTROL = 'public, s-maxage=3600, stale-while-revalidate=86400';
|
||||
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
|
||||
|
||||
const cacheValue = {
|
||||
type: 'page' as const,
|
||||
html: '<p>cached</p>',
|
||||
json: {},
|
||||
revalidate: 60,
|
||||
};
|
||||
|
||||
describe('IncrementalCacheWorker fetch', () => {
|
||||
const selfFetch = mock();
|
||||
|
||||
beforeEach(() => {
|
||||
selfFetch.mockReset();
|
||||
get.mockReset();
|
||||
getTagsFromValue.mockReset();
|
||||
hasBeenRevalidated.mockReset();
|
||||
getTagsFromValue.mockReturnValue(['space:1']);
|
||||
hasBeenRevalidated.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
const fetch = (request: Request) =>
|
||||
IncrementalCacheWorker.prototype.fetch.call(
|
||||
{
|
||||
env: { WORKER_SELF_REFERENCE: { fetch: selfFetch } },
|
||||
ctx: {},
|
||||
},
|
||||
request
|
||||
);
|
||||
|
||||
it('forwards cache reads to the internal endpoint and restores cache metadata', async () => {
|
||||
selfFetch.mockResolvedValue(
|
||||
Response.json(
|
||||
{ value: cacheValue, lastModified: 123 },
|
||||
{
|
||||
headers: {
|
||||
'x-gitbook-cache-control': CACHE_CONTROL,
|
||||
'x-gitbook-cache-tag': 'incremental-cache:entry,space:1',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/?key=entry&cacheType=cache')
|
||||
);
|
||||
|
||||
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
const forwardedRequest = selfFetch.mock.calls[0]?.[0] as Request;
|
||||
const forwardedURL = new URL(forwardedRequest.url);
|
||||
expect(forwardedURL.pathname).toBe('/internal');
|
||||
expect(forwardedURL.searchParams.get('key')).toBe('entry');
|
||||
expect(forwardedURL.searchParams.get('cacheType')).toBe('cache');
|
||||
});
|
||||
|
||||
it('reads and annotates a cache hit only on the internal endpoint', async () => {
|
||||
get.mockResolvedValue({ value: cacheValue, lastModified: Date.now() });
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=cache')
|
||||
);
|
||||
|
||||
expect(get).toHaveBeenCalledWith('entry', 'cache');
|
||||
expect(selfFetch).not.toHaveBeenCalled();
|
||||
expect(response.headers.get('cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('x-gitbook-cache-control')).toBe(CACHE_CONTROL);
|
||||
expect(response.headers.get('cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
expect(response.headers.get('x-gitbook-cache-tag')).toBe('incremental-cache:entry,space:1');
|
||||
});
|
||||
|
||||
it('keeps cache misses out of the worker cache', async () => {
|
||||
get.mockResolvedValue(null);
|
||||
|
||||
const response = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=missing')
|
||||
);
|
||||
|
||||
expect(response.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(response.headers.get('x-gitbook-cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(await response.json()).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps revalidated and stale entries out of the worker cache', async () => {
|
||||
get.mockResolvedValueOnce({ value: cacheValue, lastModified: Date.now() });
|
||||
hasBeenRevalidated.mockResolvedValueOnce(true);
|
||||
const revalidatedResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=revalidated')
|
||||
);
|
||||
|
||||
get.mockResolvedValueOnce({
|
||||
value: { ...cacheValue, revalidate: 0 },
|
||||
lastModified: Date.now() - 1_000,
|
||||
});
|
||||
const staleResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=stale')
|
||||
);
|
||||
|
||||
expect(revalidatedResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(revalidatedResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
|
||||
expect(staleResponse.headers.get('cache-control')).toBe(NO_STORE_CACHE_CONTROL);
|
||||
expect(staleResponse.headers.get('x-gitbook-cache-revalidated')).toBe('true');
|
||||
});
|
||||
|
||||
it('rejects invalid internal requests and does not forward non-GET requests', async () => {
|
||||
const invalidResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/internal?key=entry&cacheType=invalid')
|
||||
);
|
||||
const methodResponse = await fetch(
|
||||
new Request('https://incremental-cache.internal/?key=entry', { method: 'POST' })
|
||||
);
|
||||
|
||||
expect(invalidResponse.status).toBe(400);
|
||||
expect(methodResponse.status).toBe(405);
|
||||
expect(selfFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
NextModeTagCacheWriteInput,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getTagsFromValue } from '@opennextjs/aws/utils/cache.js';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
|
||||
import { GitbookIncrementalCache } from '../incrementalCache/incrementalCache';
|
||||
import tagCache from '../tagCache/middleware';
|
||||
|
||||
type CacheWorkerEnv = {
|
||||
NEXT_INC_CACHE_R2_BUCKET: {
|
||||
put(key: string, value: string): Promise<unknown>;
|
||||
};
|
||||
WORKER_SELF_REFERENCE: {
|
||||
fetch(request: Request): Promise<Response>;
|
||||
};
|
||||
};
|
||||
|
||||
//@ts-ignore - Just to avoid tag cache crashing
|
||||
globalThis.openNextConfig = {
|
||||
dangerous: {
|
||||
enableCacheInterception: true,
|
||||
},
|
||||
};
|
||||
|
||||
const cacheEntryTypes = new Set<CacheEntryType>(['cache', 'fetch', 'composable']);
|
||||
|
||||
const isCacheEntryType = (value: string | null): value is CacheEntryType =>
|
||||
value !== null && cacheEntryTypes.has(value as CacheEntryType);
|
||||
|
||||
const NO_STORE_CACHE_CONTROL = 'private, no-store, max-age=0, must-revalidate';
|
||||
const INTERNAL_PATH = '/internal';
|
||||
const CACHE_CONTROL_HEADER = 'x-gitbook-cache-control';
|
||||
const CACHE_TAG_HEADER = 'x-gitbook-cache-tag';
|
||||
|
||||
const getCacheHeaders = (cacheControl: string, cacheTag?: string): HeadersInit => ({
|
||||
'Cache-Control': cacheControl,
|
||||
[CACHE_CONTROL_HEADER]: cacheControl,
|
||||
...(cacheTag
|
||||
? {
|
||||
'Cache-Tag': cacheTag,
|
||||
[CACHE_TAG_HEADER]: cacheTag,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
const nullCacheResponse = (hasBeenRevalidated = false): Response =>
|
||||
Response.json(null, {
|
||||
headers: {
|
||||
...getCacheHeaders(NO_STORE_CACHE_CONTROL),
|
||||
...(hasBeenRevalidated ? { 'x-gitbook-cache-revalidated': 'true' } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const restoreCacheHeaders = (response: Response): Response => {
|
||||
const headers = new Headers(response.headers);
|
||||
const cacheControl = headers.get(CACHE_CONTROL_HEADER);
|
||||
const cacheTag = headers.get(CACHE_TAG_HEADER);
|
||||
|
||||
if (cacheControl) {
|
||||
headers.set('Cache-Control', cacheControl);
|
||||
}
|
||||
if (cacheTag) {
|
||||
headers.set('Cache-Tag', cacheTag);
|
||||
}
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
|
||||
const isTimeStale = (value: CacheValue<CacheEntryType>, lastModified?: number): boolean => {
|
||||
const revalidate = value.revalidate;
|
||||
if (typeof revalidate !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() >= (lastModified ?? Date.now()) + revalidate * 1000;
|
||||
};
|
||||
|
||||
const getTagName = (tag: NextModeTagCacheWriteInput): string =>
|
||||
typeof tag === 'string' ? tag : tag.tag;
|
||||
|
||||
// `use cache` can write the same key concurrently, but R2 accepts one write per key per second.
|
||||
export class R2WriteBuffer extends DurableObject<CacheWorkerEnv> {
|
||||
private writePromise: Promise<unknown> | undefined;
|
||||
|
||||
async write(cacheKey: string, value: string): Promise<void> {
|
||||
if (this.writePromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writePromise = this.env.NEXT_INC_CACHE_R2_BUCKET.put(cacheKey, value);
|
||||
this.writePromise = writePromise;
|
||||
this.ctx.waitUntil(
|
||||
writePromise.finally(() => {
|
||||
this.writePromise = undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
|
||||
|
||||
// @ts-ignore Generated by the Cloudflare build.
|
||||
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
|
||||
|
||||
export default class IncrementalCacheWorker extends WorkerEntrypoint<CacheWorkerEnv> {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('Method not allowed', { status: 405 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname !== INTERNAL_PATH) {
|
||||
url.pathname = INTERNAL_PATH;
|
||||
const response = await this.env.WORKER_SELF_REFERENCE.fetch(new Request(url, request));
|
||||
return restoreCacheHeaders(response);
|
||||
}
|
||||
|
||||
const key = url.searchParams.get('key');
|
||||
const cacheType = url.searchParams.get('cacheType');
|
||||
if (!key || (cacheType !== null && !isCacheEntryType(cacheType))) {
|
||||
return new Response('Invalid cache request', { status: 400 });
|
||||
}
|
||||
|
||||
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
|
||||
const value = await new GitbookIncrementalCache().get(key, cacheType ?? undefined);
|
||||
if (!value?.value) {
|
||||
return nullCacheResponse();
|
||||
}
|
||||
|
||||
const tags = getTagsFromValue(value?.value as CacheValue<'cache'> | undefined);
|
||||
if (await tagCache.hasBeenRevalidated(tags, value.lastModified)) {
|
||||
return nullCacheResponse(true);
|
||||
}
|
||||
|
||||
if (isTimeStale(value.value, value.lastModified)) {
|
||||
return nullCacheResponse(true);
|
||||
}
|
||||
|
||||
return Response.json(value, {
|
||||
headers: getCacheHeaders(
|
||||
// 1 hour cache, with a 1 day stale-while-revalidate.
|
||||
'public, s-maxage=3600, stale-while-revalidate=86400',
|
||||
[`incremental-cache:${key}`, ...tags].join(',')
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
await this.#runRpcOperation(async () => {
|
||||
await new GitbookIncrementalCache().set(key, value, cacheType);
|
||||
});
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await this.#runRpcOperation(async () => {
|
||||
await new GitbookIncrementalCache().delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
async writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void> {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#runRpcOperation(async () => {
|
||||
await tagCache.writeTags(tagsToWrite);
|
||||
await this.ctx.cache?.purge({ tags: tagsToWrite.map(getTagName) });
|
||||
});
|
||||
}
|
||||
|
||||
async #runRpcOperation(operation: () => Promise<void>): Promise<void> {
|
||||
await runWithCloudflareRequestContext(
|
||||
new Request('https://incremental-cache.internal'),
|
||||
this.env,
|
||||
this.ctx,
|
||||
async () => {
|
||||
await operation();
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"main": "do.js",
|
||||
"main": "do.ts",
|
||||
"name": "gitbook-open-v2-do",
|
||||
"compatibility_date": "2025-04-14",
|
||||
"compatibility_date": "2026-06-14",
|
||||
"keep_names": false,
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat",
|
||||
@@ -11,10 +11,17 @@
|
||||
"observability": {
|
||||
"enabled": false,
|
||||
},
|
||||
"cache": {
|
||||
"enabled": true,
|
||||
},
|
||||
"placement": {
|
||||
"region": "gcp:us-central1",
|
||||
},
|
||||
"env": {
|
||||
"preview": {
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_BUILD_ID": "local",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
@@ -26,7 +33,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-preview",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
@@ -52,6 +59,56 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
"vars": {
|
||||
"STAGE": "preview",
|
||||
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true",
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "gitbook-open-v2-cache-preview",
|
||||
},
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "NEXT_CACHE_DO_QUEUE",
|
||||
"class_name": "DOQueueHandler",
|
||||
},
|
||||
{
|
||||
"name": "NEXT_TAG_CACHE_DO_SHARDED",
|
||||
"class_name": "DOShardedTagCache",
|
||||
},
|
||||
{
|
||||
"name": "WRITE_BUFFER",
|
||||
"class_name": "R2WriteBuffer",
|
||||
},
|
||||
],
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "R2WriteBuffer"],
|
||||
},
|
||||
],
|
||||
"observability": {
|
||||
"traces": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
"logs": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
"STAGE": "staging",
|
||||
@@ -71,7 +128,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-staging",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
@@ -120,7 +177,7 @@
|
||||
"services": [
|
||||
{
|
||||
"binding": "WORKER_SELF_REFERENCE",
|
||||
"service": "gitbook-open-v2-production",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"NEXT_PRIVATE_DEBUG_CACHE": "true",
|
||||
// When deployed locally, we don't have access to the tag cache here,
|
||||
// we should just bypass the cache to go to the server directly
|
||||
"SHOULD_BYPASS_CACHE": "true",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771",
|
||||
},
|
||||
@@ -44,6 +41,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-dev",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-dev",
|
||||
},
|
||||
],
|
||||
},
|
||||
"preview": {
|
||||
@@ -67,6 +68,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-preview",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-preview",
|
||||
},
|
||||
],
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
@@ -87,6 +92,16 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
"observability": {
|
||||
"traces": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
"logs": {
|
||||
"enabled": true,
|
||||
"head_sampling_rate": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
"staging": {
|
||||
"vars": {
|
||||
@@ -119,6 +134,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-staging",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-staging",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
@@ -188,6 +207,10 @@
|
||||
"binding": "DEFAULT_WORKER",
|
||||
"service": "gitbook-open-v2-server-production",
|
||||
},
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_WORKER",
|
||||
"service": "gitbook-open-v2-do-production",
|
||||
},
|
||||
],
|
||||
"tail_consumers": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const getCloudflareContext = mock();
|
||||
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
|
||||
|
||||
const { GitbookIncrementalCache } = await import('./cacheWorkerClient');
|
||||
|
||||
const cacheValue = {
|
||||
type: 'page' as const,
|
||||
html: '<p>cached</p>',
|
||||
json: {},
|
||||
};
|
||||
|
||||
describe('GitbookIncrementalCache cache worker client', () => {
|
||||
const fetch = mock();
|
||||
const set = mock();
|
||||
const remove = mock();
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.mockReset();
|
||||
set.mockReset();
|
||||
remove.mockReset();
|
||||
getCloudflareContext.mockReturnValue({
|
||||
env: {
|
||||
NEXT_INC_CACHE_WORKER: { fetch, set, delete: remove },
|
||||
},
|
||||
});
|
||||
console.error = mock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('gets cache entries through the service binding', async () => {
|
||||
fetch.mockResolvedValue(
|
||||
Response.json({
|
||||
value: cacheValue,
|
||||
lastModified: 123,
|
||||
})
|
||||
);
|
||||
|
||||
const result = await new GitbookIncrementalCache().get('key with / characters', 'cache');
|
||||
|
||||
expect(result).toEqual({ value: cacheValue, lastModified: 123 });
|
||||
const request = fetch.mock.calls[0]?.[0] as Request;
|
||||
const url = new URL(request.url);
|
||||
expect(url.searchParams.get('key')).toBe('key with / characters');
|
||||
expect(url.searchParams.get('cacheType')).toBe('cache');
|
||||
});
|
||||
|
||||
it('returns null for cache misses and failed reads', async () => {
|
||||
fetch.mockResolvedValue(Response.json(null));
|
||||
expect(await new GitbookIncrementalCache().get('missing')).toBeNull();
|
||||
|
||||
fetch.mockResolvedValueOnce(new Response(null, { status: 503 }));
|
||||
expect(await new GitbookIncrementalCache().get('unavailable-response')).toBeNull();
|
||||
|
||||
fetch.mockRejectedValueOnce(new Error('service unavailable'));
|
||||
expect(await new GitbookIncrementalCache().get('unavailable')).toBeNull();
|
||||
});
|
||||
|
||||
it('uses RPC for writes and deletes', async () => {
|
||||
set.mockResolvedValue(undefined);
|
||||
remove.mockResolvedValue(undefined);
|
||||
const cache = new GitbookIncrementalCache();
|
||||
|
||||
await cache.set('entry', cacheValue, 'cache');
|
||||
await cache.delete('entry');
|
||||
|
||||
expect(set).toHaveBeenCalledWith('entry', cacheValue, 'cache');
|
||||
expect(remove).toHaveBeenCalledWith('entry');
|
||||
});
|
||||
|
||||
it('contains mutation failures', async () => {
|
||||
set.mockRejectedValue(new Error('service unavailable'));
|
||||
remove.mockRejectedValue(new Error('service unavailable'));
|
||||
const cache = new GitbookIncrementalCache();
|
||||
|
||||
await expect(cache.set('entry', cacheValue, 'cache')).resolves.toBeUndefined();
|
||||
await expect(cache.delete('entry')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import type {
|
||||
CacheEntryType,
|
||||
CacheValue,
|
||||
IncrementalCache,
|
||||
WithLastModified,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
|
||||
export const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
|
||||
|
||||
type CacheWorker = {
|
||||
fetch(request: Request): Promise<Response>;
|
||||
set<CacheType extends CacheEntryType>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void>;
|
||||
delete(key: string): Promise<void>;
|
||||
};
|
||||
|
||||
export class GitbookIncrementalCache implements IncrementalCache {
|
||||
name = 'GitbookIncrementalCache';
|
||||
|
||||
async get<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
cacheType?: CacheType
|
||||
): Promise<WithLastModified<CacheValue<CacheType>> | null> {
|
||||
try {
|
||||
const url = new URL('https://incremental-cache.internal');
|
||||
url.searchParams.set('key', key);
|
||||
if (cacheType) {
|
||||
url.searchParams.set('cacheType', cacheType);
|
||||
}
|
||||
|
||||
const response = await this.getWorker().fetch(new Request(url));
|
||||
if (!response.ok) {
|
||||
console.error('Failed to get from cache worker', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await response.json()) as WithLastModified<CacheValue<CacheType>> | null;
|
||||
} catch (error) {
|
||||
console.error('Failed to get from cache worker', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set<CacheType extends CacheEntryType = 'cache'>(
|
||||
key: string,
|
||||
value: CacheValue<CacheType>,
|
||||
cacheType?: CacheType
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.getWorker().set(key, value, cacheType);
|
||||
} catch (error) {
|
||||
console.error('Failed to set to cache worker', error);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
try {
|
||||
await this.getWorker().delete(key);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete from cache worker', error);
|
||||
}
|
||||
}
|
||||
|
||||
private getWorker(): CacheWorker {
|
||||
const env = getCloudflareContext().env as Record<string, unknown>;
|
||||
const worker = env[BINDING_NAME] as CacheWorker | undefined;
|
||||
if (!worker) {
|
||||
throw new Error(`Missing ${BINDING_NAME} service binding`);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
}
|
||||
|
||||
export default new GitbookIncrementalCache();
|
||||
@@ -0,0 +1,51 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
import { DEFAULT_PREFIX, GitbookIncrementalCache } from './incrementalCache';
|
||||
|
||||
const environmentKeys = ['OPEN_NEXT_BUILD_ID', 'DEPLOYMENT_ID', 'NEXT_BUILD_ID'] as const;
|
||||
const originalEnvironment = Object.fromEntries(
|
||||
environmentKeys.map((key) => [key, process.env[key]])
|
||||
);
|
||||
|
||||
const hash = (key: string) => createHash('sha256').update(key).digest('hex');
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of environmentKeys) {
|
||||
const value = originalEnvironment[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('GitbookIncrementalCache cache keys', () => {
|
||||
it('uses the deployment-aware OpenNext build ID', () => {
|
||||
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
|
||||
process.env.NEXT_BUILD_ID = 'legacy-build-id';
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
|
||||
`${DEFAULT_PREFIX}/deployment-id/${hash('entry')}.cache`
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the Cloudflare deployment ID when the worker build ID is unavailable', () => {
|
||||
delete process.env.OPEN_NEXT_BUILD_ID;
|
||||
process.env.DEPLOYMENT_ID = 'runtime-deployment-id';
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key('entry')).toBe(
|
||||
`${DEFAULT_PREFIX}/runtime-deployment-id/${hash('entry')}.cache`
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes composable cache keys before applying the deployment namespace', () => {
|
||||
process.env.OPEN_NEXT_BUILD_ID = 'deployment-id';
|
||||
const key = JSON.stringify(['next-build-id', 'cache-key']);
|
||||
|
||||
expect(new GitbookIncrementalCache().getR2Key(key, 'composable')).toBe(
|
||||
`${DEFAULT_PREFIX}/dataCache/${hash(JSON.stringify(['cache-key']))}.composable`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -128,7 +128,8 @@ export class GitbookIncrementalCache implements IncrementalCache {
|
||||
}
|
||||
|
||||
const hash = createHash('sha256').update(key).digest('hex');
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? process.env?.NEXT_BUILD_ID : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
const buildId = process.env.OPEN_NEXT_BUILD_ID ?? process.env.DEPLOYMENT_ID;
|
||||
return `${DEFAULT_PREFIX}/${cacheType === 'cache' ? buildId : 'dataCache'}/${hash}.${cacheType}`.replace(
|
||||
/\/+/g,
|
||||
'/'
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
import { GitbookIncrementalCache } from './cacheWorkerClient';
|
||||
|
||||
import { GitbookIncrementalCache } from './incrementalCache';
|
||||
// export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
// mode: 'long-lived',
|
||||
// // We can do it because we use our own logic to invalidate the cache
|
||||
// bypassTagCacheOnCacheHit: true,
|
||||
// //TODO: bump it again once I figured out the race condition
|
||||
// defaultLongLivedTtlSec: 5 * 60, // 5 minutes
|
||||
// // We don't want to update the cache entry on every cache hit
|
||||
// shouldLazilyUpdateOnCacheHit: false,
|
||||
// });
|
||||
|
||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
mode: 'long-lived',
|
||||
// We can do it because we use our own logic to invalidate the cache
|
||||
bypassTagCacheOnCacheHit: true,
|
||||
//TODO: bump it again once I figured out the race condition
|
||||
defaultLongLivedTtlSec: 5 * 60, // 5 minutes
|
||||
// We don't want to update the cache entry on every cache hit
|
||||
shouldLazilyUpdateOnCacheHit: false,
|
||||
});
|
||||
export default new GitbookIncrementalCache();
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
// import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache';
|
||||
import { GitbookIncrementalCache } from './cacheWorkerClient';
|
||||
|
||||
import { GitbookIncrementalCache } from './incrementalCache';
|
||||
// // We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
|
||||
// // and cause race conditions. This will be fixed in a future release of OpenNext
|
||||
// export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
// mode: 'long-lived',
|
||||
// // Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
|
||||
// // TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
|
||||
// bypassTagCacheOnCacheHit: false,
|
||||
// //TODO: remove, reducing cache ttl of regional cache to help debugging
|
||||
// defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
|
||||
// // We don't want to update the cache entry on every cache hit
|
||||
// shouldLazilyUpdateOnCacheHit: false,
|
||||
// });
|
||||
|
||||
// We cannot have regional cache only in the middleware, otherwise it will override things on cache miss
|
||||
// and cause race conditions. This will be fixed in a future release of OpenNext
|
||||
export default withRegionalCache(new GitbookIncrementalCache(), {
|
||||
mode: 'long-lived',
|
||||
// Because of a race condition, the middleware may have populated the cache entry before `cache.match` had time to run on the server.
|
||||
// TODO: We should bypass the incremental cache entirely when the interceptor has caught the request. Should be done in OpenNext.
|
||||
bypassTagCacheOnCacheHit: false,
|
||||
//TODO: remove, reducing cache ttl of regional cache to help debugging
|
||||
defaultLongLivedTtlSec: 5 * 60 /* 5 minutes */,
|
||||
// We don't want to update the cache entry on every cache hit
|
||||
shouldLazilyUpdateOnCacheHit: false,
|
||||
});
|
||||
export default new GitbookIncrementalCache();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
const getCloudflareContext = mock();
|
||||
mock.module('@opennextjs/cloudflare', () => ({ getCloudflareContext }));
|
||||
|
||||
const { default: tagCache } = await import('./cacheWorkerClient');
|
||||
|
||||
describe('GitbookTagCache cache worker client', () => {
|
||||
const writeTags = mock();
|
||||
|
||||
beforeEach(() => {
|
||||
writeTags.mockReset();
|
||||
getCloudflareContext.mockReturnValue({
|
||||
env: {
|
||||
NEXT_INC_CACHE_WORKER: { writeTags },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
getCloudflareContext.mockReset();
|
||||
});
|
||||
|
||||
it('writes hard tags through the cache worker service binding', async () => {
|
||||
writeTags.mockResolvedValue(undefined);
|
||||
|
||||
await tagCache.writeTags([
|
||||
'content',
|
||||
{ tag: 'with-duration', stale: 100, expire: 200 },
|
||||
'_N_T_/soft-tag',
|
||||
]);
|
||||
|
||||
expect(writeTags).toHaveBeenCalledWith([
|
||||
'content',
|
||||
{ tag: 'with-duration', stale: 100, expire: 200 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not call the worker when all tags are soft tags', async () => {
|
||||
await tagCache.writeTags(['_N_T_/soft-tag']);
|
||||
|
||||
expect(writeTags).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { NextModeTagCache, NextModeTagCacheWriteInput } from '@opennextjs/aws/types/overrides';
|
||||
import { getCloudflareContext } from '@opennextjs/cloudflare';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
const BINDING_NAME = 'NEXT_INC_CACHE_WORKER';
|
||||
|
||||
type CacheWorker = {
|
||||
writeTags(tags: NextModeTagCacheWriteInput[]): Promise<void>;
|
||||
};
|
||||
|
||||
const getWorker = (): CacheWorker => {
|
||||
const env = getCloudflareContext().env as Record<string, unknown>;
|
||||
const worker = env[BINDING_NAME] as CacheWorker | undefined;
|
||||
if (!worker) {
|
||||
throw new Error(`Missing ${BINDING_NAME} service binding`);
|
||||
}
|
||||
return worker;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'GitbookTagCache',
|
||||
mode: 'nextMode',
|
||||
// Do nothing.
|
||||
getLastRevalidated: async () => {
|
||||
return 0;
|
||||
},
|
||||
// Return false, everything handled at the incremental cache level in the do worker.
|
||||
hasBeenRevalidated: async () => {
|
||||
return false;
|
||||
},
|
||||
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await getWorker().writeTags(tagsToWrite);
|
||||
},
|
||||
} satisfies NextModeTagCache;
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { NextModeTagCache } from '@opennextjs/aws/types/overrides.js';
|
||||
import type {
|
||||
NextModeTagCache,
|
||||
NextModeTagCacheWriteInput,
|
||||
} from '@opennextjs/aws/types/overrides.js';
|
||||
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
|
||||
import { softTagFilter } from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
|
||||
|
||||
@@ -48,7 +51,7 @@ export default {
|
||||
return false; // In case of error, return false
|
||||
}
|
||||
},
|
||||
writeTags: async (tags: string[]) => {
|
||||
writeTags: async (tags: NextModeTagCacheWriteInput[]) => {
|
||||
const tagsToWrite = tags.filter(softTagFilter);
|
||||
if (tagsToWrite.length === 0) {
|
||||
const logger = getLogger().subLogger('gitbookTagCache');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@base-ui/react": "catalog:",
|
||||
"@cloudflare/workers-types": "^4.20251011.0",
|
||||
"@cloudflare/workers-types": "^5.20260716.1",
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
@@ -62,7 +62,7 @@
|
||||
"quick-lru": "^7.0.1",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"react-hotkeys-hook": "^5.3.3",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
@@ -89,39 +89,37 @@
|
||||
"@argos-ci/playwright": "^7.2.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@scalar/api-client-react": "catalog:",
|
||||
"@tailwindcss/cli": "4.1.11",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsontoxml": "^1.0.5",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"@types/node": "^20",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/parse-cache-control": "^1.0.4",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/rison": "^0.0.9",
|
||||
"@types/negotiator": "^0.6.4",
|
||||
"bun-types": "catalog:",
|
||||
"deepmerge": "^4.3.1",
|
||||
"diff": "^8.0.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"postcss": "^8",
|
||||
"rss-parser": "^3.13.0",
|
||||
"stylelint": "^16.16.0",
|
||||
"stylelint-no-unsupported-browser-features": "^8.1.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"ts-essentials": "^10.0.1",
|
||||
"typescript": "catalog:",
|
||||
"vercel": "50.37.3",
|
||||
"wrangler": "^4.79.0"
|
||||
"wrangler": "^4.122.0",
|
||||
"rss-parser": "^3.13.0"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "./scripts/generate.sh",
|
||||
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts && bun ./scripts/generate-block-css.ts && bun ./scripts/download-fonts.ts",
|
||||
"generate:fonts": "bun ./scripts/generate-font-faces.ts",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar && rm -rf ./public/~gitbook/static/css && rm -rf ./public/~gitbook/static/fonts",
|
||||
"generate:assets": "bun ./scripts/generate-mermaid-runtime.ts && bun ./scripts/generate-scalar-runtime.ts",
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math && rm -rf ./public/~gitbook/static/mermaid && rm -rf ./public/~gitbook/static/scalar",
|
||||
"dev": "bun run generate:assets && env-cmd --silent -f ../../.env.local next --webpack",
|
||||
"build": "bun run generate:assets && next build --webpack",
|
||||
"build:local": "bun run generate:assets && GITBOOK_URL=http://localhost:3000 next build --webpack",
|
||||
@@ -133,6 +131,7 @@
|
||||
"dev:cf:middleware": "wrangler dev --port 8771 --inspector-port 9230 --env dev --config ./openNext/customWorkers/middlewareWrangler.jsonc",
|
||||
"dev:cf:server": "wrangler dev --port 8772 --env dev --config ./openNext/customWorkers/defaultWrangler.jsonc",
|
||||
"profile:cf:memory": "bun run build:cloudflare && bun ./scripts/profile-opennext-memory.ts",
|
||||
"dev:cf:cache": "wrangler dev --env dev --config ./openNext/customWorkers/doWrangler.jsonc",
|
||||
"e2e": "playwright test e2e/internal.spec.ts e2e/cookie-banner.spec.ts e2e/pdf.spec.ts e2e/select.spec.ts --project=chromium",
|
||||
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
|
||||
"e2e-style-perf": "playwright test e2e/style-perf.spec.ts --project=chromium --reporter=list",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { copyFile, mkdir, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import sourcesData from '../src/fonts/generated/sources.json' with { type: 'json' };
|
||||
import type { FontSourcesData } from '../src/fonts/types';
|
||||
|
||||
const CONCURRENCY = 8;
|
||||
const ATTEMPTS = 3;
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const fontsDir = join(scriptDir, '../src/fonts');
|
||||
const outputDir = join(scriptDir, '../public/~gitbook/static/fonts');
|
||||
|
||||
const { google, local } = sourcesData as FontSourcesData;
|
||||
const sources = new Map<string, string>(Object.entries(local));
|
||||
for (const [googleId, { prefix, files }] of Object.entries(google)) {
|
||||
for (const file of files) {
|
||||
sources.set(`${googleId}/${file}`, `${prefix}/${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Faces move between releases; stale files would otherwise pile up in the deployed assets.
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
for (const entry of await readdir(outputDir, { recursive: true, withFileTypes: true })) {
|
||||
if (entry.isFile()) {
|
||||
const file = relative(outputDir, join(entry.parentPath, entry.name));
|
||||
if (!sources.has(file)) {
|
||||
await rm(join(outputDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pending = [...sources].filter(([file]) => !Bun.file(join(outputDir, file)).size);
|
||||
if (pending.length > 0) {
|
||||
console.log(`Downloading ${pending.length} font files…`);
|
||||
}
|
||||
|
||||
const queue = pending.values();
|
||||
await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
|
||||
|
||||
async function worker() {
|
||||
for (const [file, source] of queue) {
|
||||
const target = join(outputDir, file);
|
||||
await mkdir(dirname(target), { recursive: true });
|
||||
|
||||
if (source.startsWith('./')) {
|
||||
await copyFile(join(fontsDir, source), target);
|
||||
continue;
|
||||
}
|
||||
|
||||
await writeFile(target, await fetchWithRetries(source));
|
||||
}
|
||||
}
|
||||
|
||||
// Google Fonts intermittently refuses connections when a build asks for hundreds of files at once,
|
||||
// and a single miss fails the whole build.
|
||||
async function fetchWithRetries(url: string): Promise<Buffer> {
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
}
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
} catch (error) {
|
||||
if (attempt >= ATTEMPTS) {
|
||||
throw new Error(`Unable to download ${url}: ${error}`);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { mkdir, rm } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { BLOCK_STYLESHEETS } from '../src/components/DocumentView/blockStylesheets';
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const sourceDir = join(scriptDir, '../src');
|
||||
const outputDir = join(scriptDir, '../public/~gitbook/static/css');
|
||||
const tailwindCli = join(
|
||||
dirname(createRequire(import.meta.url).resolve('@tailwindcss/cli/package.json')),
|
||||
'dist/index.mjs'
|
||||
);
|
||||
|
||||
await rm(outputDir, { force: true, recursive: true });
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
|
||||
for (const [name, source] of Object.entries(BLOCK_STYLESHEETS)) {
|
||||
const result = Bun.spawnSync(
|
||||
[
|
||||
'node',
|
||||
tailwindCli,
|
||||
'--input',
|
||||
join(sourceDir, source),
|
||||
'--output',
|
||||
join(outputDir, `${name}.css`),
|
||||
'--minify',
|
||||
],
|
||||
{ stdout: 'pipe', stderr: 'pipe' }
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Unable to build ${source}: ${result.stderr.toString()}`);
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { ABC_FAVORIT, FONT_DEFINITIONS, type FontDefinition } from '../src/fonts/definitions';
|
||||
import type {
|
||||
FontFacesData,
|
||||
FontFallbackFaceData,
|
||||
FontSourcesData,
|
||||
FontVariantData,
|
||||
} from '../src/fonts/types';
|
||||
|
||||
// Google Fonts picks the file format from the user agent — the same modern Chrome `next/font` sends,
|
||||
// so we keep getting compact woff2 (and vector rather than bitmap emoji).
|
||||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36';
|
||||
|
||||
// The precalculated metrics `next/font/google` uses, so the fallback faces stay identical.
|
||||
const { calculateSizeAdjustValues } = createRequire(import.meta.url)(
|
||||
'next/dist/server/font-utils'
|
||||
) as {
|
||||
calculateSizeAdjustValues: (family: string) => {
|
||||
ascent: string;
|
||||
descent: string;
|
||||
lineGap: string;
|
||||
fallbackFont: string;
|
||||
sizeAdjust: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ResolvedFace = {
|
||||
weight: string;
|
||||
style: string;
|
||||
file: string;
|
||||
source: string;
|
||||
unicodeRange: string;
|
||||
};
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const faces: FontFacesData = {};
|
||||
const sources: FontSourcesData = { google: {}, local: {} };
|
||||
|
||||
for (const [name, definition] of Object.entries(FONT_DEFINITIONS)) {
|
||||
const resolved = definition.googleId
|
||||
? await getGoogleFaces(definition)
|
||||
: await getABCFavoritFaces();
|
||||
|
||||
if (resolved.length === 0) {
|
||||
throw new Error(`No font faces resolved for ${name}`);
|
||||
}
|
||||
|
||||
recordSources(definition, resolved);
|
||||
|
||||
const subsets = [...new Set(resolved.map((face) => face.unicodeRange))];
|
||||
const variants = new Map<string, FontVariantData>();
|
||||
|
||||
for (const face of resolved) {
|
||||
const key = `${face.weight}|${face.style}`;
|
||||
let variant = variants.get(key);
|
||||
if (!variant) {
|
||||
variant = { weight: face.weight, style: face.style, files: [] };
|
||||
variants.set(key, variant);
|
||||
}
|
||||
variant.files[subsets.indexOf(face.unicodeRange)] = face.file;
|
||||
}
|
||||
|
||||
faces[name] = {
|
||||
family: definition.family,
|
||||
variable: definition.variable,
|
||||
fontFamilyValue: [
|
||||
`"${definition.family}"`,
|
||||
...(definition.adjustFallback ? [`"${definition.family} Fallback"`] : []),
|
||||
...definition.fallback,
|
||||
].join(','),
|
||||
subsets,
|
||||
variants: [...variants.values()],
|
||||
fallbackFace: definition.adjustFallback ? getFallbackFace(name, definition.family) : null,
|
||||
...(definition.googleId ? {} : { ascentOverride: ABC_FAVORIT.ascentOverride }),
|
||||
};
|
||||
}
|
||||
|
||||
const generatedDir = join(scriptDir, '../src/fonts/generated');
|
||||
await writeFile(join(generatedDir, 'faces.json'), `${JSON.stringify(faces, null, 4)}\n`);
|
||||
await writeFile(join(generatedDir, 'sources.json'), `${JSON.stringify(sources, null, 4)}\n`);
|
||||
|
||||
/** Google serves every file of a family from one versioned directory, so only the names differ. */
|
||||
function recordSources(definition: FontDefinition, resolved: ResolvedFace[]) {
|
||||
if (!definition.googleId) {
|
||||
for (const face of resolved) {
|
||||
sources.local[face.file] = face.source;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const prefixes = new Set(
|
||||
resolved.map((face) => face.source.slice(0, face.source.lastIndexOf('/')))
|
||||
);
|
||||
if (prefixes.size !== 1) {
|
||||
throw new Error(`${definition.family} spans several Google Fonts directories`);
|
||||
}
|
||||
|
||||
sources.google[definition.googleId] = {
|
||||
prefix: [...prefixes][0] as string,
|
||||
files: [...new Set(resolved.map((face) => basename(face.file)))],
|
||||
};
|
||||
}
|
||||
|
||||
async function getGoogleFaces(definition: FontDefinition): Promise<ResolvedFace[]> {
|
||||
const { family, googleId, weights } = definition;
|
||||
const url = `https://fonts.googleapis.com/css2?family=${family.replaceAll(' ', '+')}:wght@${weights.join(';')}&display=swap`;
|
||||
|
||||
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unable to fetch ${family} from Google Fonts: ${response.status} (${url})`);
|
||||
}
|
||||
|
||||
const resolved = [...(await response.text()).matchAll(/@font-face\s*\{([^}]*)\}/g)].map(
|
||||
(match) => {
|
||||
const block = match[1] ?? '';
|
||||
const source = read(block, 'src')?.match(/url\((https:[^)]+\.woff2)\)/)?.[1];
|
||||
const weight = read(block, 'font-weight');
|
||||
const unicodeRange = read(block, 'unicode-range');
|
||||
|
||||
if (!source || !weight || !unicodeRange) {
|
||||
throw new Error(`Unexpected @font-face for ${family}: ${block}`);
|
||||
}
|
||||
|
||||
return {
|
||||
weight,
|
||||
style: read(block, 'font-style') ?? 'normal',
|
||||
// Google's filenames are content-addressed, so the asset can stay immutable.
|
||||
file: `${googleId}/${basename(new URL(source).pathname)}`,
|
||||
source,
|
||||
unicodeRange: unicodeRange.toLowerCase(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const missing = weights.filter((weight) => !resolved.some((face) => face.weight === weight));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`Google Fonts returned no ${missing.join('/')} weight for ${family}`);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function getABCFavoritFaces(): Promise<ResolvedFace[]> {
|
||||
return Promise.all(
|
||||
ABC_FAVORIT.sources.map(async (source) => {
|
||||
const path = join(scriptDir, '../src/fonts/ABCFavorit', source.file);
|
||||
const digest = createHash('sha256')
|
||||
.update(await readFile(path))
|
||||
.digest('hex');
|
||||
|
||||
return {
|
||||
weight: source.weight,
|
||||
style: source.style,
|
||||
file: `abcfavorit/${digest.slice(0, 16)}.woff2`,
|
||||
source: `./ABCFavorit/${source.file}`,
|
||||
unicodeRange: '',
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getFallbackFace(name: string, family: string): FontFallbackFaceData {
|
||||
if (name === 'ABCFavorit') {
|
||||
return { family: `${family} Fallback`, local: 'Arial', ...ABC_FAVORIT.fallbackMetrics };
|
||||
}
|
||||
|
||||
const metrics = calculateSizeAdjustValues(family);
|
||||
return {
|
||||
family: `${family} Fallback`,
|
||||
local: metrics.fallbackFont,
|
||||
ascentOverride: `${metrics.ascent}%`,
|
||||
descentOverride: `${metrics.descent}%`,
|
||||
lineGapOverride: `${metrics.lineGap}%`,
|
||||
sizeAdjust: `${metrics.sizeAdjust}%`,
|
||||
};
|
||||
}
|
||||
|
||||
function read(block: string, property: string): string | undefined {
|
||||
return block.match(new RegExp(`${property}\\s*:\\s*([^;]+);`))?.[1]?.trim();
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Usage:
|
||||
* bun packages/gitbook/scripts/test-cache-revalidation.ts benchmark
|
||||
* bun packages/gitbook/scripts/test-cache-revalidation.ts revalidate
|
||||
*/
|
||||
|
||||
import { createHmac } from 'node:crypto';
|
||||
|
||||
// Edit this section for the local site under test. `bun dev` exposes published
|
||||
// sites at /url/<published-site-host>/<path>.
|
||||
const PAGE_URLS = [
|
||||
'http://localhost:3000/url/example.gitbook.io/docs',
|
||||
'http://localhost:3000/url/example.gitbook.io/docs/getting-started',
|
||||
];
|
||||
|
||||
// The revalidation route must use the same /url/<published-site-host>/ prefix.
|
||||
// Leave REVALIDATE_SECRET empty when the deployment does not set GITBOOK_SECRET.
|
||||
const REVALIDATE_URL = 'http://localhost:3000/url/example.gitbook.io/~gitbook/revalidate';
|
||||
const REVALIDATE_SECRET = '';
|
||||
const REVALIDATE_BODY = '{"tags":["space:YOUR_SPACE_ID"]}';
|
||||
|
||||
// Next sets x-nextjs-cache to HIT or MISS. Change this when testing through a
|
||||
// proxy that exposes the result under a different header.
|
||||
const CACHE_STATUS_HEADER = 'x-nextjs-cache';
|
||||
const BENCHMARK_RUNS = 3;
|
||||
const WARMUP_REQUESTS = 2;
|
||||
const WAIT_AFTER_REVALIDATION_MS = 1_000;
|
||||
|
||||
type RequestResult = {
|
||||
cacheStatus: string | null;
|
||||
status: number;
|
||||
totalMs: number;
|
||||
ttfbMs: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
function printTableHeader() {
|
||||
console.log(
|
||||
`${'request'.padEnd(13)} ${'HTTP'.padEnd(4)} ${CACHE_STATUS_HEADER.padEnd(16)} ${'TTFB / total'.padEnd(22)} URL`
|
||||
);
|
||||
}
|
||||
|
||||
function printResult(label: string, result: RequestResult) {
|
||||
const cacheStatus = result.cacheStatus ?? '<missing>';
|
||||
const timings = `${result.ttfbMs.toFixed(1)} / ${result.totalMs.toFixed(1)} ms`;
|
||||
console.log(
|
||||
`${label.padEnd(13)} ${String(result.status).padEnd(4)} ${cacheStatus.padEnd(16)} ${timings.padEnd(22)} ${result.url}`
|
||||
);
|
||||
}
|
||||
|
||||
async function requestPage(url: string): Promise<RequestResult> {
|
||||
const start = performance.now();
|
||||
const response = await fetch(url);
|
||||
const ttfbMs = performance.now() - start;
|
||||
|
||||
// Consume the response body so totalMs measures the entire transfer.
|
||||
await response.arrayBuffer();
|
||||
|
||||
return {
|
||||
cacheStatus: response.headers.get(CACHE_STATUS_HEADER),
|
||||
status: response.status,
|
||||
totalMs: performance.now() - start,
|
||||
ttfbMs,
|
||||
url: response.url,
|
||||
};
|
||||
}
|
||||
|
||||
function isSuccessful(result: RequestResult) {
|
||||
return result.status >= 200 && result.status < 400;
|
||||
}
|
||||
|
||||
async function benchmark(): Promise<number> {
|
||||
let failures = 0;
|
||||
printTableHeader();
|
||||
|
||||
for (let run = 1; run <= BENCHMARK_RUNS; run += 1) {
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult(`benchmark-${run}`, result);
|
||||
if (!isSuccessful(result)) {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`benchmark-${run} ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
function validateRevalidationConfig() {
|
||||
if (
|
||||
REVALIDATE_URL.includes('example.gitbook.io') ||
|
||||
REVALIDATE_BODY.includes('YOUR_') ||
|
||||
PAGE_URLS.some((url) => url.includes('example.gitbook.io'))
|
||||
) {
|
||||
throw new Error(
|
||||
'Set PAGE_URLS, REVALIDATE_URL, and REVALIDATE_BODY before revalidate mode.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createSignature() {
|
||||
if (!REVALIDATE_SECRET) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return createHmac('sha256', REVALIDATE_SECRET).update(REVALIDATE_BODY).digest('hex');
|
||||
}
|
||||
|
||||
async function revalidate(): Promise<number> {
|
||||
validateRevalidationConfig();
|
||||
let failures = 0;
|
||||
|
||||
console.log(`Warming each page (${WARMUP_REQUESTS} request(s) each)...`);
|
||||
printTableHeader();
|
||||
for (let run = 1; run <= WARMUP_REQUESTS; run += 1) {
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult(`warmup-${run}`, result);
|
||||
if (!isSuccessful(result)) {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`warmup-${run} ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const headers = new Headers({ 'content-type': 'application/json' });
|
||||
const signature = createSignature();
|
||||
if (signature) {
|
||||
headers.set('x-gitbook-signature', signature);
|
||||
}
|
||||
|
||||
console.log(`Revalidating ${REVALIDATE_BODY}...`);
|
||||
const revalidationResponse = await fetch(REVALIDATE_URL, {
|
||||
body: REVALIDATE_BODY,
|
||||
headers,
|
||||
method: 'POST',
|
||||
});
|
||||
const revalidationResponseBody = await revalidationResponse.text();
|
||||
if (!revalidationResponse.ok) {
|
||||
throw new Error(
|
||||
`Revalidation failed with ${revalidationResponse.status}: ${revalidationResponseBody}`
|
||||
);
|
||||
}
|
||||
console.log(revalidationResponseBody);
|
||||
|
||||
await Bun.sleep(WAIT_AFTER_REVALIDATION_MS);
|
||||
|
||||
console.log('Checking that the next response is a MISS...');
|
||||
printTableHeader();
|
||||
for (const url of PAGE_URLS) {
|
||||
try {
|
||||
const result = await requestPage(url);
|
||||
printResult('revalidated', result);
|
||||
if (!isSuccessful(result) || result.cacheStatus?.toUpperCase() !== 'MISS') {
|
||||
failures += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`revalidated ERR ${url}: ${String(error)}`);
|
||||
failures += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const mode = process.argv[2] ?? 'benchmark';
|
||||
let failures: number;
|
||||
|
||||
try {
|
||||
switch (mode) {
|
||||
case 'benchmark':
|
||||
failures = await benchmark();
|
||||
break;
|
||||
case 'revalidate':
|
||||
failures = await revalidate();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Usage: bun ${process.argv[1]} {benchmark|revalidate}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(String(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`${failures} request(s) failed.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void main().then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
});
|
||||
@@ -63,9 +63,7 @@ export function AIChat() {
|
||||
() => {
|
||||
chatController.close();
|
||||
},
|
||||
{
|
||||
useKey: true,
|
||||
}
|
||||
[]
|
||||
);
|
||||
|
||||
// Track the view of the AI chat
|
||||
|
||||
@@ -77,9 +77,9 @@ export function AIChatInput(props: {
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
|
||||
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
|
||||
// instead of focusing the Assistant input. RND-11340.
|
||||
useKey: true,
|
||||
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AdminToolbarLazy } from './AdminToolbarLazy';
|
||||
import { AdminToolbarClient } from './AdminToolbarClient';
|
||||
import { getToolbarChangedPages } from './changedPages';
|
||||
import type { AdminToolbarContext } from './types';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
@@ -62,5 +62,5 @@ export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
changedPages,
|
||||
};
|
||||
|
||||
return <AdminToolbarLazy context={minimalContext} />;
|
||||
return <AdminToolbarClient context={minimalContext} />;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import type { AdminToolbarClientProps } from './types';
|
||||
|
||||
// The toolbar only ever appears after hydration (`useToolbarVisibility` reads cookies and local
|
||||
// storage), so nothing is lost by deferring it — and it takes its CSS modules out of the route's
|
||||
// render-blocking stylesheets, where every published page was paying for them.
|
||||
const AdminToolbarClient = dynamic(
|
||||
() => import('./AdminToolbarClient').then((mod) => mod.AdminToolbarClient),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export function AdminToolbarLazy(props: AdminToolbarClientProps) {
|
||||
return <AdminToolbarClient {...props} />;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// Only the server entrypoint: re-exporting the client modules would pull their CSS modules back
|
||||
// into the route's render-blocking stylesheets, defeating the lazy boundary.
|
||||
export * from './AdminToolbar';
|
||||
export * from './AdminToolbarClient';
|
||||
export * from './IframeWrapper';
|
||||
export * from './Toolbar';
|
||||
export * from './transitions';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { BlockStylesheetName } from './blockStylesheets';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
import { buildVersion } from '@/lib/build';
|
||||
|
||||
export function getBlockStylesheetURL(name: BlockStylesheetName): string {
|
||||
return getAssetURL(`css/${name}.css?v=${buildVersion()}`);
|
||||
}
|
||||
|
||||
// `href` + `precedence` opt into React's stylesheet hoisting: the tag moves to `<head>`, dedupes
|
||||
// across every block on the page, and holds paint until it loads, so the block never flashes
|
||||
// unstyled. `blocks` sorts after Next's own `next` precedence, keeping the previous cascade order.
|
||||
export function BlockStylesheet(props: { name: BlockStylesheetName }) {
|
||||
return <link rel="stylesheet" href={getBlockStylesheetURL(props.name)} precedence="blocks" />;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import type { DocumentBlockIntegration, RenderIntegrationUI } from '@gitbook/api
|
||||
import { ContentKit, ContentKitOutput } from '@gitbook/react-contentkit';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { BlockStylesheet } from '../BlockStylesheet';
|
||||
import { getWebframePageContext, integrationBlockContainsWebframe } from './adaptive';
|
||||
import { contentKitServerContext } from './contentkit';
|
||||
import './contentkit.css';
|
||||
import {
|
||||
ContentKitWithClientContext,
|
||||
type WebframeLinkerData,
|
||||
@@ -105,7 +105,6 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
|
||||
|
||||
return (
|
||||
<div className={tcls(style)}>
|
||||
<BlockStylesheet name="contentkit" />
|
||||
{useClientContext ? (
|
||||
<ContentKitWithClientContext
|
||||
{...contentKitProps}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { BlockStylesheet } from '../BlockStylesheet';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
|
||||
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
export async function OpenAPIOperation(props: BlockProps<AnyOpenAPIOperationsBlock>) {
|
||||
const { style } = props;
|
||||
return (
|
||||
<>
|
||||
<BlockStylesheet name="openapi" />
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIOperationBody {...props} />
|
||||
</div>
|
||||
</>
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIOperationBody {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { BlockStylesheet } from '../BlockStylesheet';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPISchemasBlock } from '@/lib/openapi/resolveOpenAPISchemasBlock';
|
||||
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
export async function OpenAPISchemas(props: BlockProps<OpenAPISchemasBlock>) {
|
||||
const { style } = props;
|
||||
return (
|
||||
<>
|
||||
<BlockStylesheet name="openapi" />
|
||||
<div className={tcls('flex w-full', style, 'max-w-full')}>
|
||||
<OpenAPISchemasBody {...props} />
|
||||
</div>
|
||||
</>
|
||||
<div className={tcls('flex w-full', style, 'max-w-full')}>
|
||||
<OpenAPISchemasBody {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BlockProps } from '../Block';
|
||||
import { BlockStylesheet } from '../BlockStylesheet';
|
||||
import { getOpenAPIBlockClientProps } from './context';
|
||||
import { OpenAPIBlockLazy } from './OpenAPIBlockLazy';
|
||||
import { resolveOpenAPIWebhookBlock } from '@/lib/openapi/resolveOpenAPIWebhookBlock';
|
||||
@@ -12,12 +11,9 @@ import { tcls } from '@/lib/tailwind';
|
||||
export async function OpenAPIWebhook(props: BlockProps<OpenAPIWebhookBlock>) {
|
||||
const { style } = props;
|
||||
return (
|
||||
<>
|
||||
<BlockStylesheet name="openapi" />
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIWebhookBody {...props} />
|
||||
</div>
|
||||
</>
|
||||
<div className={tcls('flex w-full min-w-0', style, 'max-w-full')}>
|
||||
<OpenAPIWebhookBody {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PlainCodeBlock } from '../CodeBlock';
|
||||
import { DocumentView } from '../DocumentView';
|
||||
import { Heading } from '../Heading';
|
||||
import type { OpenAPIBlockClientContextProps } from './OpenAPIBlockClient';
|
||||
import './style.css';
|
||||
import { SCALAR_RUNTIME_PATH } from './scalar-runtime-path';
|
||||
import { DEFAULT_LOCALE, getSpaceLocale } from '@/intl/server';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Built as standalone assets by `scripts/generate-block-css.ts` rather than imported: an import
|
||||
// would put them in the route's render-blocking CSS on every page, block or no block.
|
||||
// Values are the sources to compile, relative to `src/`.
|
||||
export const BLOCK_STYLESHEETS = {
|
||||
openapi: 'components/DocumentView/OpenAPI/style.css',
|
||||
contentkit: 'components/DocumentView/Integration/contentkit.css',
|
||||
} as const;
|
||||
|
||||
export type BlockStylesheetName = keyof typeof BLOCK_STYLESHEETS;
|
||||
@@ -30,11 +30,8 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
data-gb-site-footer
|
||||
className={tcls(
|
||||
'border-tint-subtle border-t',
|
||||
// If the footer only contains a mode toggle, we only show it where the outline
|
||||
// column — which carries its own toggle — isn't pinned open.
|
||||
mobileOnly
|
||||
? 'layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden page-api-block:page-has-outline:min-[96rem]:hidden'
|
||||
: null
|
||||
// If the footer only contains a mode toggle, we only show it on smaller screens
|
||||
mobileOnly ? 'xl:hidden' : null
|
||||
)}
|
||||
>
|
||||
<div className="lg:chat-open:pr-(--ai-chat-width) transition-[padding] duration-300 motion-reduce:transition-none">
|
||||
@@ -99,9 +96,7 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
{
|
||||
// Theme Toggle
|
||||
customization.themes.toggeable ? (
|
||||
// Hidden where the outline column is pinned open (see PageAside),
|
||||
// since that column carries its own toggle.
|
||||
<div className="page-api-block:page-has-outline:min-[96rem]:hidden -col-start-2 row-start-1 flex items-start @xs:justify-end layout-default:xl:chat-closed:hidden layout-default:3xl:hidden layout-wide:3xl:chat-closed:hidden">
|
||||
<div className="-col-start-2 row-start-1 flex items-start @xs:justify-end xl:hidden">
|
||||
<React.Suspense fallback={null}>
|
||||
<ThemeToggler />
|
||||
</React.Suspense>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../primitives/DropdownMenu';
|
||||
import { SiteAuthLoginDropdownMenuItem } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import { HeaderLinkMoreDropdown } from './HeaderLinkMoreClient';
|
||||
import styles from './headerLinks.module.css';
|
||||
import { isSiteAuthLoginHref } from '@/lib/auth-login-link';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
@@ -33,7 +34,7 @@ export function HeaderLinkMore(props: {
|
||||
const { label, links, context, socialAccounts } = props;
|
||||
|
||||
return (
|
||||
<div className="header-links-ellipsis z-20 items-center">
|
||||
<div className={`${styles.linkEllipsis} z-20 items-center`}>
|
||||
<HeaderLinkMoreDropdown
|
||||
label={label}
|
||||
dropdownClassName={tcls(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type React from 'react';
|
||||
|
||||
import styles from './headerLinks.module.css';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
|
||||
interface HeaderLinksProps {
|
||||
@@ -11,7 +12,7 @@ export async function HeaderLinks({ children, style }: HeaderLinksProps) {
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'header-links',
|
||||
styles.containerHeaderlinks,
|
||||
'@4xl:[&>.button+.button]:-ml-2 z-20 ml-auto flex min-w-9 shrink grow @7xl:grow-0 items-center justify-end @4xl:gap-x-6 gap-x-4',
|
||||
style
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
@container header (width < 1280px) {
|
||||
.containerHeaderlinks {
|
||||
container-type: inline-size;
|
||||
container-name: headerlinks;
|
||||
}
|
||||
}
|
||||
|
||||
.linkEllipsis {
|
||||
display: none;
|
||||
& div > a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container headerlinks (width < 150px) {
|
||||
.containerHeaderlinks > :nth-child(n + 1) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 1) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 1) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 300px) {
|
||||
.containerHeaderlinks > :nth-child(n + 2) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 2) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 2) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 450px) {
|
||||
.containerHeaderlinks > :nth-child(n + 3) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 3) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 3) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 550px) {
|
||||
.containerHeaderlinks > :nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 4) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 4) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 650px) {
|
||||
.containerHeaderlinks > :nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 5) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 5) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 750px) {
|
||||
.containerHeaderlinks > :nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
.containerHeaderlinks > :nth-child(n + 6) ~ .linkEllipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 6) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import { sanitizeGitBookAppURL } from '@/lib/app';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||
import { getPageDocument } from '@/lib/data';
|
||||
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
|
||||
import './pdf.css';
|
||||
import { resolvePageId } from '@/lib/pages';
|
||||
import { getLinkerForSiteSpace } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CustomizationDefaultThemeMode } from '@gitbook/api';
|
||||
|
||||
import { PRINT_STYLES } from './printStyles';
|
||||
import { CustomizationRootLayout } from '@/components/RootLayout';
|
||||
import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
|
||||
|
||||
@@ -18,9 +17,6 @@ export async function PDFRootLayout(props: {
|
||||
context={context}
|
||||
forcedTheme={CustomizationDefaultThemeMode.Light}
|
||||
>
|
||||
<style href="pdf-print" precedence="blocks">
|
||||
{PRINT_STYLES}
|
||||
</style>
|
||||
{children}
|
||||
</CustomizationRootLayout>
|
||||
);
|
||||
|
||||
-4
@@ -1,6 +1,3 @@
|
||||
// Inlined rather than imported as CSS: a `.css` import lands in the route's render-blocking
|
||||
// stylesheets, and webpack was attaching this one to the content pages too.
|
||||
export const PRINT_STYLES = `
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 60pt 60pt;
|
||||
@@ -59,4 +56,3 @@ export const PRINT_STYLES = `
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { getBlockStylesheetURL } from '../DocumentView/BlockStylesheet';
|
||||
import { CurrentPageProvider } from '../hooks/useCurrentPage';
|
||||
import { TrackPageViewEvent } from '../Insights';
|
||||
import { CONTENT_STYLE } from '../layout';
|
||||
@@ -72,14 +69,6 @@ export async function PageBody(props: {
|
||||
// to API-reference pages only (matching the `page-api-block` styling), so other pages keep the
|
||||
// header structure untouched.
|
||||
const hasAPIBlocks = document ? hasAPIBlock(document) : false;
|
||||
|
||||
// The block renders its own stylesheet link, but that only reaches the browser once the block
|
||||
// streams in — start the request from the shell instead. Top-level blocks only, which is the
|
||||
// common case; nested ones still get the sheet from the link.
|
||||
if (hasAPIBlocks) {
|
||||
ReactDOM.preload(getBlockStylesheetURL('openapi'), { as: 'style' });
|
||||
}
|
||||
|
||||
const wideLayout = wideContent || page.layout.width === 'wide';
|
||||
const language = await getSpaceLanguage(context);
|
||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||
|
||||
@@ -26,12 +26,8 @@ import { AnnouncementDismissedScript } from '../Announcement';
|
||||
import { SelectStateScript } from '../Select';
|
||||
import { OperatingSystemClassScript } from './OperatingSystemClassScript';
|
||||
import { RootLayoutClientContexts } from './RootLayoutClientContexts';
|
||||
import {
|
||||
DEFAULT_MONOSPACE_FONT,
|
||||
type FontData,
|
||||
generateEmojiFontFacesCSS,
|
||||
getFontData,
|
||||
} from '@/fonts';
|
||||
import { type FontData, getFontData } from '@/fonts';
|
||||
import { fontNotoColorEmoji, fonts } from '@/fonts/default';
|
||||
import './globals.css';
|
||||
import { getContentLocale, getSpaceLanguage } from '@/intl/server';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
@@ -95,10 +91,12 @@ export async function CustomizationRootLayout(props: {
|
||||
const fontData = getFontData(customization.styling.font, 'content');
|
||||
// Temporarily add a if here while the cache is being warmed up.
|
||||
// We can remove the condition after 14-07-2025.
|
||||
const monospaceFontData = getFontData(
|
||||
customization.styling.monospaceFont ?? DEFAULT_MONOSPACE_FONT,
|
||||
'mono'
|
||||
);
|
||||
const monospaceFontData = customization.styling.monospaceFont
|
||||
? getFontData(customization.styling.monospaceFont, 'mono')
|
||||
: {
|
||||
type: 'default' as const,
|
||||
variable: fonts.IBMPlexMono.variable,
|
||||
};
|
||||
|
||||
// Preconnect and preload custom fonts if needed
|
||||
preloadFont(fontData);
|
||||
@@ -129,8 +127,10 @@ export async function CustomizationRootLayout(props: {
|
||||
sidebarStyles.list && `sidebar-list-${sidebarStyles.list}`,
|
||||
'links' in customization.styling && `links-${customization.styling.links}`,
|
||||
'depth' in customization.styling && `depth-${customization.styling.depth}`,
|
||||
typeof customization.styling.font === 'string'
|
||||
? `font-${customization.styling.font}`
|
||||
fontNotoColorEmoji.variable,
|
||||
monospaceFontData.type === 'default' ? monospaceFontData.variable : null,
|
||||
fontData.type === 'default'
|
||||
? [fontData.variable, `font-${customization.styling.font}`]
|
||||
: null,
|
||||
|
||||
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
|
||||
@@ -150,10 +150,11 @@ export async function CustomizationRootLayout(props: {
|
||||
{/* Apply the visitor's content selection to <html> before first paint (no flash) */}
|
||||
<SelectStateScript />
|
||||
|
||||
{/* Only the picked families, so the head never carries every font we support */}
|
||||
<style>{generateEmojiFontFacesCSS()}</style>
|
||||
<style>{fontData.fontFaceRules}</style>
|
||||
<style>{monospaceFontData.fontFaceRules}</style>
|
||||
{/* Inject custom font @font-face rules */}
|
||||
{fontData.type === 'custom' ? <style>{fontData.fontFaceRules}</style> : null}
|
||||
{monospaceFontData.type === 'custom' ? (
|
||||
<style>{monospaceFontData.fontFaceRules}</style>
|
||||
) : null}
|
||||
|
||||
{/* Inject a script to detect if the announcmeent banner has been dismissed */}
|
||||
{'announcement' in customization && customization.announcement?.enabled ? (
|
||||
|
||||
@@ -552,104 +552,3 @@ html.dark .highlight-line.diff-deleted .highlight-line-content::before {
|
||||
.button-group > :is(button, a):not(:first-child):has(~ :is(button, a)) {
|
||||
@apply rounded-none!;
|
||||
}
|
||||
|
||||
/* Zoomable images */
|
||||
html:has(.zoom-modal) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zoom-img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.zoom-image-active {
|
||||
view-transition-name: zoom-image;
|
||||
}
|
||||
|
||||
.zoom-modal img {
|
||||
view-transition-name: zoom-image;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
/* Header links collapse into the "more" dropdown one by one as the header narrows. Each breakpoint
|
||||
hides one more link and reveals the ellipsis holding the ones that no longer fit. */
|
||||
@container header (width < 1280px) {
|
||||
.header-links {
|
||||
container-type: inline-size;
|
||||
container-name: headerlinks;
|
||||
}
|
||||
}
|
||||
|
||||
.header-links-ellipsis {
|
||||
display: none;
|
||||
& div > a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container headerlinks (width < 150px) {
|
||||
.header-links > :nth-child(n + 1) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 1) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 1) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 300px) {
|
||||
.header-links > :nth-child(n + 2) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 2) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 2) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 450px) {
|
||||
.header-links > :nth-child(n + 3) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 3) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 3) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 550px) {
|
||||
.header-links > :nth-child(n + 4) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 4) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 4) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 650px) {
|
||||
.header-links > :nth-child(n + 5) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 5) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 5) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@container headerlinks (width < 750px) {
|
||||
.header-links > :nth-child(n + 6) {
|
||||
display: none;
|
||||
}
|
||||
.header-links > :nth-child(n + 6) ~ .header-links-ellipsis {
|
||||
display: flex;
|
||||
& div > *:nth-child(n + 6) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,10 @@ export function SearchContainer({
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
// Match the produced key, not the physical key position, so non-QWERTY
|
||||
// layouts don't trigger the shortcut by position. RND-11340.
|
||||
useKey: true,
|
||||
// Match the logical character typed, not the physical key position, so
|
||||
// non-QWERTY layouts don't trigger the shortcut by position (e.g. on
|
||||
// Dvorak the physical "K"/"I" keys produce other characters). RND-11340.
|
||||
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'k',
|
||||
}
|
||||
);
|
||||
|
||||
@@ -93,9 +94,9 @@ export function SearchContainer({
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
// Match the produced key so Dvorak ⌘-C (physical "I" key) copies
|
||||
// Match the logical character so Dvorak ⌘-C (physical "I" key) copies
|
||||
// instead of opening the Assistant. RND-11340.
|
||||
useKey: true,
|
||||
ignoreEventWhen: (e) => e.key.toLowerCase() !== 'i',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -46,22 +46,11 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
} = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const isFrame = mode === 'frame';
|
||||
const isInitialMountRef = useRef(true);
|
||||
|
||||
const language = useLanguage();
|
||||
|
||||
useEffect(() => {
|
||||
const isInitialMount = isInitialMountRef.current;
|
||||
isInitialMountRef.current = false;
|
||||
|
||||
if (!isOpen) {
|
||||
// A user can focus the SSR'd input natively before hydration attaches
|
||||
// React's focus handler; syncing to "open" instead of blurring avoids
|
||||
// stealing that focus back on mount.
|
||||
if (isInitialMount && document.activeElement === inputRef.current) {
|
||||
onFocus?.();
|
||||
return;
|
||||
}
|
||||
inputRef.current?.blur();
|
||||
return;
|
||||
}
|
||||
@@ -80,7 +69,7 @@ export const SearchInput = React.forwardRef<HTMLDivElement, SearchInputProps>(
|
||||
|
||||
const timeout = window.setTimeout(focusInput, 150);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [isFrame, isOpen, value.length, onFocus]);
|
||||
}, [isFrame, isOpen, value.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
html:has(.zoomModal) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zoomImg {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.zoomImageActive {
|
||||
view-transition-name: zoom-image;
|
||||
}
|
||||
|
||||
.zoomModal {
|
||||
/** Unstyled */
|
||||
}
|
||||
|
||||
.zoomModal img {
|
||||
view-transition-name: zoom-image;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import styles from './ZoomImage.module.css';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
const PADDING = 32; // Padding around the image in the modal, in pixels
|
||||
@@ -158,8 +159,8 @@ export function ZoomImage(
|
||||
}}
|
||||
className={classNames(
|
||||
props.className,
|
||||
zoomable ? 'zoom-img' : null,
|
||||
active ? 'zoom-image-active' : null
|
||||
zoomable ? styles.zoomImg : null,
|
||||
active ? styles.zoomImageActive : null
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -199,7 +200,7 @@ function ZoomImageModal(props: {
|
||||
<div
|
||||
data-testid="zoom-image-modal"
|
||||
className={classNames(
|
||||
'zoom-modal',
|
||||
styles.zoomModal,
|
||||
tcls(
|
||||
'fixed',
|
||||
'inset-0',
|
||||
|
||||
@@ -1,83 +1,297 @@
|
||||
import { CustomizationDefaultMonospaceFont } from '@gitbook/api';
|
||||
import {
|
||||
DM_Mono,
|
||||
Fira_Code,
|
||||
Fira_Sans_Extra_Condensed,
|
||||
IBM_Plex_Mono,
|
||||
IBM_Plex_Serif,
|
||||
Inconsolata,
|
||||
Inter,
|
||||
JetBrains_Mono,
|
||||
Lato,
|
||||
Merriweather,
|
||||
Noto_Color_Emoji,
|
||||
Noto_Sans,
|
||||
Open_Sans,
|
||||
Overpass,
|
||||
Poppins,
|
||||
Raleway,
|
||||
Roboto,
|
||||
Roboto_Mono,
|
||||
Roboto_Slab,
|
||||
Source_Code_Pro,
|
||||
Source_Sans_3,
|
||||
Space_Mono,
|
||||
Ubuntu,
|
||||
} from 'next/font/google';
|
||||
import localFont from 'next/font/local';
|
||||
|
||||
import { EMOJI_FONT, type FontName } from './definitions';
|
||||
import faces from './generated/faces.json';
|
||||
import type { FontFacesData, FontFamilyData } from './types';
|
||||
import { getAssetURL } from '@/lib/assets';
|
||||
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
|
||||
|
||||
const fontFaces = faces as FontFacesData;
|
||||
export const fontNotoColorEmoji = Noto_Color_Emoji({
|
||||
variable: '--font-noto-color-emoji',
|
||||
weight: ['400'],
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
// The rules only vary by font, and every page renders a few dozen of them.
|
||||
const cache = new Map<FontName, string>();
|
||||
/*
|
||||
Fonts are downloaded and loaded by next/font.
|
||||
|
||||
/** Used until the cache has warmed up for sites saved before the setting existed. */
|
||||
export const DEFAULT_MONOSPACE_FONT = CustomizationDefaultMonospaceFont.IBMPlexMono;
|
||||
We can't use "preload: true" as otherwise Next will preload all the fonts on the page
|
||||
while spaces only use one font at a time.
|
||||
*/
|
||||
|
||||
// Emitted per picked family and inlined in the head, so a page never carries the other 20 families
|
||||
// the way a shared `next/font` stylesheet did.
|
||||
export function generateDefaultFontFacesCSS(font: FontName): string {
|
||||
const cached = cache.get(font);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const inter = Inter({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const family = fontFaces[font];
|
||||
if (!family) {
|
||||
throw new Error(`Missing generated font faces for ${font}`);
|
||||
}
|
||||
const firaSans = Fira_Sans_Extra_Condensed({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const css = [
|
||||
...family.variants.flatMap((variant) =>
|
||||
variant.files.map((file, subset) =>
|
||||
generateFace(family, variant.weight, variant.style, file, family.subsets[subset])
|
||||
)
|
||||
),
|
||||
generateFallbackFace(family),
|
||||
`:root { ${family.variable}: ${family.fontFamilyValue}; }`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
const ibmPlexSerif = IBM_Plex_Serif({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['serif'],
|
||||
});
|
||||
|
||||
cache.set(font, css);
|
||||
return css;
|
||||
}
|
||||
const lato = Lato({
|
||||
weight: ['400', '700', '900'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
export function generateEmojiFontFacesCSS(): string {
|
||||
return generateDefaultFontFacesCSS(EMOJI_FONT);
|
||||
}
|
||||
const merriweather = Merriweather({
|
||||
weight: ['400', '700', '900'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['serif'],
|
||||
});
|
||||
|
||||
function generateFace(
|
||||
family: FontFamilyData,
|
||||
weight: string,
|
||||
style: string,
|
||||
file: string,
|
||||
unicodeRange: string | undefined
|
||||
): string {
|
||||
return `@font-face {
|
||||
font-family: "${family.family}";
|
||||
font-style: ${style};
|
||||
font-weight: ${weight};
|
||||
font-display: swap;${family.ascentOverride ? `\n ascent-override: ${family.ascentOverride};` : ''}
|
||||
src: url(${getAssetURL(`fonts/${file}`)}) format("woff2");${
|
||||
unicodeRange ? `\n unicode-range: ${unicodeRange};` : ''
|
||||
}
|
||||
}`;
|
||||
}
|
||||
const notoSans = Noto_Sans({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
// A `local()` face with the real font's metrics: the page keeps the system font's glyphs but the
|
||||
// picked font's proportions, so swapping it in barely shifts the layout.
|
||||
function generateFallbackFace(family: FontFamilyData): string {
|
||||
const fallback = family.fallbackFace;
|
||||
if (!fallback) {
|
||||
return '';
|
||||
}
|
||||
const openSans = Open_Sans({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
return `@font-face {
|
||||
font-family: "${fallback.family}";
|
||||
src: local("${fallback.local}");
|
||||
ascent-override: ${fallback.ascentOverride};
|
||||
descent-override: ${fallback.descentOverride};
|
||||
line-gap-override: ${fallback.lineGapOverride};
|
||||
size-adjust: ${fallback.sizeAdjust};
|
||||
}`;
|
||||
}
|
||||
const overpass = Overpass({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const poppins = Poppins({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const raleway = Raleway({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const roboto = Roboto({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const robotoSlab = Roboto_Slab({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const sourceSansPro = Source_Sans_3({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const ubuntu = Ubuntu({
|
||||
weight: ['400', '500', '700'],
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
});
|
||||
|
||||
const abcFavorit = localFont({
|
||||
variable: '--font-content',
|
||||
preload: false,
|
||||
display: 'swap',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
src: [
|
||||
{
|
||||
path: './ABCFavorit/ABCFavorit-Variable.woff2',
|
||||
weight: '400 700',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: './ABCFavorit/ABCFavorit-BoldItalic.woff2',
|
||||
weight: '700',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './ABCFavorit/ABCFavorit-MediumItalic.woff2',
|
||||
weight: '500',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: './ABCFavorit/ABCFavorit-RegularItalic.woff2',
|
||||
weight: '400',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
declarations: [{ prop: 'ascent-override', value: '100%' }],
|
||||
});
|
||||
|
||||
const ibmPlexMono = IBM_Plex_Mono({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const dmMono = DM_Mono({
|
||||
weight: ['400', '500'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const firaCode = Fira_Code({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const inconsolata = Inconsolata({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const jetBrainsMono = JetBrains_Mono({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const robotoMono = Roboto_Mono({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const sourceCodePro = Source_Code_Pro({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
const spaceMono = Space_Mono({
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-mono',
|
||||
style: 'normal',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
fallback: ['monospace'],
|
||||
adjustFontFallback: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Font definitions.
|
||||
*/
|
||||
export const fonts: {
|
||||
[fontName in CustomizationDefaultFont | CustomizationDefaultMonospaceFont]: {
|
||||
variable: string;
|
||||
};
|
||||
} = {
|
||||
[CustomizationDefaultFont.Inter]: inter,
|
||||
[CustomizationDefaultFont.FiraSans]: firaSans,
|
||||
[CustomizationDefaultFont.IBMPlexSerif]: ibmPlexSerif,
|
||||
[CustomizationDefaultFont.Lato]: lato,
|
||||
[CustomizationDefaultFont.Merriweather]: merriweather,
|
||||
[CustomizationDefaultFont.NotoSans]: notoSans,
|
||||
[CustomizationDefaultFont.OpenSans]: openSans,
|
||||
[CustomizationDefaultFont.Overpass]: overpass,
|
||||
[CustomizationDefaultFont.Poppins]: poppins,
|
||||
[CustomizationDefaultFont.Raleway]: raleway,
|
||||
[CustomizationDefaultFont.Roboto]: roboto,
|
||||
[CustomizationDefaultFont.RobotoSlab]: robotoSlab,
|
||||
[CustomizationDefaultFont.SourceSansPro]: sourceSansPro,
|
||||
[CustomizationDefaultFont.Ubuntu]: ubuntu,
|
||||
[CustomizationDefaultFont.ABCFavorit]: abcFavorit,
|
||||
[CustomizationDefaultMonospaceFont.IBMPlexMono]: ibmPlexMono,
|
||||
[CustomizationDefaultMonospaceFont.DMMono]: dmMono,
|
||||
[CustomizationDefaultMonospaceFont.FiraCode]: firaCode,
|
||||
[CustomizationDefaultMonospaceFont.Inconsolata]: inconsolata,
|
||||
[CustomizationDefaultMonospaceFont.JetBrainsMono]: jetBrainsMono,
|
||||
[CustomizationDefaultMonospaceFont.RobotoMono]: robotoMono,
|
||||
[CustomizationDefaultMonospaceFont.SourceCodePro]: sourceCodePro,
|
||||
[CustomizationDefaultMonospaceFont.SpaceMono]: spaceMono,
|
||||
};
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import { CustomizationDefaultFont, CustomizationDefaultMonospaceFont } from '@gitbook/api';
|
||||
|
||||
/** The emoji font is always loaded, alongside whichever content and monospace fonts a site picked. */
|
||||
export const EMOJI_FONT = 'NotoColorEmoji';
|
||||
|
||||
export type FontName =
|
||||
| CustomizationDefaultFont
|
||||
| CustomizationDefaultMonospaceFont
|
||||
| typeof EMOJI_FONT;
|
||||
|
||||
export interface FontDefinition {
|
||||
/** `font-family` to declare the faces under. */
|
||||
family: string;
|
||||
/** Family id on Google Fonts, or `null` for the fonts we ship ourselves. */
|
||||
googleId: string | null;
|
||||
weights: string[];
|
||||
/** CSS variable the family is exposed as. */
|
||||
variable: '--font-content' | '--font-mono' | '--font-noto-color-emoji';
|
||||
/** Families appended after the (metric-adjusted) fallback. */
|
||||
fallback: string[];
|
||||
/** Declare a `<family> Fallback` face with metric overrides, so the swap shifts layout as
|
||||
* little as possible. Monospace fonts opt out, as they did under `next/font`. */
|
||||
adjustFallback: boolean;
|
||||
}
|
||||
|
||||
const content = (
|
||||
family: string,
|
||||
googleId: string,
|
||||
weights: string[],
|
||||
fallback: string[] = ['system-ui', 'arial']
|
||||
): FontDefinition => ({
|
||||
family,
|
||||
googleId,
|
||||
weights,
|
||||
variable: '--font-content',
|
||||
fallback,
|
||||
adjustFallback: true,
|
||||
});
|
||||
|
||||
const mono = (family: string, googleId: string, weights: string[]): FontDefinition => ({
|
||||
family,
|
||||
googleId,
|
||||
weights,
|
||||
variable: '--font-mono',
|
||||
fallback: ['monospace'],
|
||||
adjustFallback: false,
|
||||
});
|
||||
|
||||
export const FONT_DEFINITIONS: Record<FontName, FontDefinition> = {
|
||||
[CustomizationDefaultFont.Inter]: content('Inter', 'inter', ['400', '500', '600', '700']),
|
||||
[CustomizationDefaultFont.FiraSans]: content(
|
||||
'Fira Sans Extra Condensed',
|
||||
'fira-sans-extra-condensed',
|
||||
['400', '500', '600', '700']
|
||||
),
|
||||
[CustomizationDefaultFont.IBMPlexSerif]: content(
|
||||
'IBM Plex Serif',
|
||||
'ibm-plex-serif',
|
||||
['400', '500', '600', '700'],
|
||||
['serif']
|
||||
),
|
||||
[CustomizationDefaultFont.Lato]: content('Lato', 'lato', ['400', '700', '900']),
|
||||
[CustomizationDefaultFont.Merriweather]: content(
|
||||
'Merriweather',
|
||||
'merriweather',
|
||||
['400', '700', '900'],
|
||||
['serif']
|
||||
),
|
||||
[CustomizationDefaultFont.NotoSans]: content('Noto Sans', 'noto-sans', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultFont.OpenSans]: content('Open Sans', 'open-sans', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultFont.Overpass]: content('Overpass', 'overpass', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultFont.Poppins]: content('Poppins', 'poppins', ['400', '500', '600', '700']),
|
||||
[CustomizationDefaultFont.Raleway]: content('Raleway', 'raleway', ['400', '500', '600', '700']),
|
||||
[CustomizationDefaultFont.Roboto]: content('Roboto', 'roboto', ['400', '500', '600', '700']),
|
||||
[CustomizationDefaultFont.RobotoSlab]: content('Roboto Slab', 'roboto-slab', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultFont.SourceSansPro]: content('Source Sans 3', 'source-sans-3', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultFont.Ubuntu]: content('Ubuntu', 'ubuntu', ['400', '500', '700']),
|
||||
[CustomizationDefaultFont.ABCFavorit]: {
|
||||
family: 'abcFavorit',
|
||||
googleId: null,
|
||||
weights: [],
|
||||
variable: '--font-content',
|
||||
fallback: ['system-ui', 'arial'],
|
||||
adjustFallback: true,
|
||||
},
|
||||
|
||||
[CustomizationDefaultMonospaceFont.IBMPlexMono]: mono('IBM Plex Mono', 'ibm-plex-mono', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.DMMono]: mono('DM Mono', 'dm-mono', ['400', '500']),
|
||||
[CustomizationDefaultMonospaceFont.FiraCode]: mono('Fira Code', 'fira-code', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.Inconsolata]: mono('Inconsolata', 'inconsolata', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.JetBrainsMono]: mono('JetBrains Mono', 'jetbrains-mono', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.RobotoMono]: mono('Roboto Mono', 'roboto-mono', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.SourceCodePro]: mono('Source Code Pro', 'source-code-pro', [
|
||||
'400',
|
||||
'500',
|
||||
'600',
|
||||
'700',
|
||||
]),
|
||||
[CustomizationDefaultMonospaceFont.SpaceMono]: mono('Space Mono', 'space-mono', ['400', '700']),
|
||||
|
||||
[EMOJI_FONT]: {
|
||||
family: 'Noto Color Emoji',
|
||||
googleId: 'noto-color-emoji',
|
||||
weights: ['400'],
|
||||
variable: '--font-noto-color-emoji',
|
||||
fallback: [],
|
||||
adjustFallback: true,
|
||||
},
|
||||
};
|
||||
|
||||
// `abcFavorit` is licensed, so it ships in the repo instead of coming from Google Fonts. The
|
||||
// fallback metrics are the ones `next/font/local` measured from these exact files with fontkit.
|
||||
export const ABC_FAVORIT = {
|
||||
sources: [
|
||||
{ file: 'ABCFavorit-Variable.woff2', weight: '400 700', style: 'normal' },
|
||||
{ file: 'ABCFavorit-BoldItalic.woff2', weight: '700', style: 'italic' },
|
||||
{ file: 'ABCFavorit-MediumItalic.woff2', weight: '500', style: 'italic' },
|
||||
{ file: 'ABCFavorit-RegularItalic.woff2', weight: '400', style: 'italic' },
|
||||
],
|
||||
/** The design sits low in its em box; without this the fallback swap jumps vertically. */
|
||||
ascentOverride: '100%',
|
||||
fallbackMetrics: {
|
||||
ascentOverride: '90.97%',
|
||||
descentOverride: '37.34%',
|
||||
lineGapOverride: '0.00%',
|
||||
sizeAdjust: '104.43%',
|
||||
},
|
||||
} as const;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,341 +0,0 @@
|
||||
{
|
||||
"google": {
|
||||
"inter": {
|
||||
"prefix": "https://fonts.gstatic.com/s/inter/v20",
|
||||
"files": [
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2",
|
||||
"UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2"
|
||||
]
|
||||
},
|
||||
"fira-sans-extra-condensed": {
|
||||
"prefix": "https://fonts.gstatic.com/s/firasansextracondensed/v11",
|
||||
"files": [
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fKuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fuuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fOuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fyuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fCuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1fGuulWcrE5Hcg.woff2",
|
||||
"NaPKcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda1f-uulWcrE4.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3W-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3y-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3S-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3u-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3e-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3a-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3QyNr3i-oWR9e2U.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3W-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3y-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3S-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3u-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3e-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3a-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3SCKr3i-oWR9e2U.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3W-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3y-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3S-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3u-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3e-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3a-oWR9e2WPJQ.woff2",
|
||||
"NaPPcYDaAO5dirw6IaFn7lPJFqXmS-M9Atn3wgda3USLr3i-oWR9e2U.woff2"
|
||||
]
|
||||
},
|
||||
"ibm-plex-serif": {
|
||||
"prefix": "https://fonts.gstatic.com/s/ibmplexserif/v20",
|
||||
"files": [
|
||||
"jizDREVNn1dOx-zrZ2X3pZvkTiUS2zcZiVbJsNo.woff2",
|
||||
"jizDREVNn1dOx-zrZ2X3pZvkTiUb2zcZiVbJsNo.woff2",
|
||||
"jizDREVNn1dOx-zrZ2X3pZvkTiUQ2zcZiVbJsNo.woff2",
|
||||
"jizDREVNn1dOx-zrZ2X3pZvkTiUR2zcZiVbJsNo.woff2",
|
||||
"jizDREVNn1dOx-zrZ2X3pZvkTiUf2zcZiVbJ.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI5q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CIwq1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI7q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI6q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3s-CI0q1vjitOh.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI5q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yIwq1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI7q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI6q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi3A_yI0q1vjitOh.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI5q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iIwq1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI7q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI6q1vjitOh3oc.woff2",
|
||||
"jizAREVNn1dOx-zrZ2X3pZvkTi2k_iI0q1vjitOh.woff2"
|
||||
]
|
||||
},
|
||||
"lato": {
|
||||
"prefix": "https://fonts.gstatic.com/s/lato/v25",
|
||||
"files": [
|
||||
"S6uyw4BMUTPHjxAwXiWtFCfQ7A.woff2",
|
||||
"S6uyw4BMUTPHjx4wXiWtFCc.woff2",
|
||||
"S6u9w4BMUTPHh6UVSwaPGQ3q5d0N7w.woff2",
|
||||
"S6u9w4BMUTPHh6UVSwiPGQ3q5d0.woff2",
|
||||
"S6u9w4BMUTPHh50XSwaPGQ3q5d0N7w.woff2",
|
||||
"S6u9w4BMUTPHh50XSwiPGQ3q5d0.woff2"
|
||||
]
|
||||
},
|
||||
"merriweather": {
|
||||
"prefix": "https://fonts.gstatic.com/s/merriweather/v33",
|
||||
"files": [
|
||||
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqnJ-mXq1Gi3iE.woff2",
|
||||
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSequJ-mXq1Gi3iE.woff2",
|
||||
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqlJ-mXq1Gi3iE.woff2",
|
||||
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqkJ-mXq1Gi3iE.woff2",
|
||||
"u-4e0qyriQwlOrhSvowK_l5UcA6zuSYEqOzpPe3HOZJ5eX1WtLaQwmYiSeqqJ-mXq1Gi.woff2"
|
||||
]
|
||||
},
|
||||
"noto-sans": {
|
||||
"prefix": "https://fonts.gstatic.com/s/notosans/v42",
|
||||
"files": [
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aPdu3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5ardu3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a_du3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aLdu3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a3du3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aHdu3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5aDdu3mhPy1Fig.woff2",
|
||||
"o-0bIpQlx3QUlC5A4PNB6Ryti20_6n1iPHjc5a7du3mhPy0.woff2"
|
||||
]
|
||||
},
|
||||
"open-sans": {
|
||||
"prefix": "https://fonts.gstatic.com/s/opensans/v44",
|
||||
"files": [
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu0SC55K5gw.woff2",
|
||||
"memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2"
|
||||
]
|
||||
},
|
||||
"overpass": {
|
||||
"prefix": "https://fonts.gstatic.com/s/overpass/v19",
|
||||
"files": [
|
||||
"qFdH35WCmI96Ajtm81GoU9vgwBcIs1s.woff2",
|
||||
"qFdH35WCmI96Ajtm81GhU9vgwBcIs1s.woff2",
|
||||
"qFdH35WCmI96Ajtm81GqU9vgwBcIs1s.woff2",
|
||||
"qFdH35WCmI96Ajtm81GrU9vgwBcIs1s.woff2",
|
||||
"qFdH35WCmI96Ajtm81GlU9vgwBcI.woff2"
|
||||
]
|
||||
},
|
||||
"poppins": {
|
||||
"prefix": "https://fonts.gstatic.com/s/poppins/v24",
|
||||
"files": [
|
||||
"pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2",
|
||||
"pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2",
|
||||
"pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2",
|
||||
"pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2",
|
||||
"pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2",
|
||||
"pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2",
|
||||
"pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2"
|
||||
]
|
||||
},
|
||||
"raleway": {
|
||||
"prefix": "https://fonts.gstatic.com/s/raleway/v37",
|
||||
"files": [
|
||||
"1Ptug8zYS_SKggPNyCAIT4ttDfCmxA.woff2",
|
||||
"1Ptug8zYS_SKggPNyCkIT4ttDfCmxA.woff2",
|
||||
"1Ptug8zYS_SKggPNyCIIT4ttDfCmxA.woff2",
|
||||
"1Ptug8zYS_SKggPNyCMIT4ttDfCmxA.woff2",
|
||||
"1Ptug8zYS_SKggPNyC0IT4ttDfA.woff2"
|
||||
]
|
||||
},
|
||||
"roboto": {
|
||||
"prefix": "https://fonts.gstatic.com/s/roboto/v51",
|
||||
"files": [
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3iUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3CUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2",
|
||||
"KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2"
|
||||
]
|
||||
},
|
||||
"roboto-slab": {
|
||||
"prefix": "https://fonts.gstatic.com/s/robotoslab/v36",
|
||||
"files": [
|
||||
"BngMUXZYTXPIvIBgJJSb6ufA5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufJ5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufB5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufO5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufC5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufD5qWr4xCCQ_k.woff2",
|
||||
"BngMUXZYTXPIvIBgJJSb6ufN5qWr4xCC.woff2"
|
||||
]
|
||||
},
|
||||
"source-sans-3": {
|
||||
"prefix": "https://fonts.gstatic.com/s/sourcesans3/v19",
|
||||
"files": [
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wIaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wsaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wMaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wwaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wAaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3wEaZejf5HdF8Q.woff2",
|
||||
"nwpStKy2OAdR1K-IwhWudF-R3w8aZejf5Hc.woff2"
|
||||
]
|
||||
},
|
||||
"ubuntu": {
|
||||
"prefix": "https://fonts.gstatic.com/s/ubuntu/v21",
|
||||
"files": [
|
||||
"4iCs6KVjbNBYlgoKcg72nU6AF7xm.woff2",
|
||||
"4iCs6KVjbNBYlgoKew72nU6AF7xm.woff2",
|
||||
"4iCs6KVjbNBYlgoKcw72nU6AF7xm.woff2",
|
||||
"4iCs6KVjbNBYlgoKfA72nU6AF7xm.woff2",
|
||||
"4iCs6KVjbNBYlgoKcQ72nU6AF7xm.woff2",
|
||||
"4iCs6KVjbNBYlgoKfw72nU6AFw.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3jvWyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3jtGyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3jvGyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3js2yNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3jvmyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCjC3jsGyNPYZvgw.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjvWyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjtGyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjvGyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjs2yNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjvmyNPYZvg7UI.woff2",
|
||||
"4iCv6KVjbNBYlgoCxCvjsGyNPYZvgw.woff2"
|
||||
]
|
||||
},
|
||||
"ibm-plex-mono": {
|
||||
"prefix": "https://fonts.gstatic.com/s/ibmplexmono/v20",
|
||||
"files": [
|
||||
"-F63fjptAgt5VM-kVkqdyU8n1iIq131nj-otFQ.woff2",
|
||||
"-F63fjptAgt5VM-kVkqdyU8n1isq131nj-otFQ.woff2",
|
||||
"-F63fjptAgt5VM-kVkqdyU8n1iAq131nj-otFQ.woff2",
|
||||
"-F63fjptAgt5VM-kVkqdyU8n1iEq131nj-otFQ.woff2",
|
||||
"-F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgsAXHNk.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl1FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlRFgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl9FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwl5FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3vAOwlBFgsAXHNk.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl1FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlRFgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl9FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwl5FgsAXHNlYzg.woff2",
|
||||
"-F6qfjptAgt5VM-kVkqdyU8n3pQPwlBFgsAXHNk.woff2"
|
||||
]
|
||||
},
|
||||
"dm-mono": {
|
||||
"prefix": "https://fonts.gstatic.com/s/dmmono/v16",
|
||||
"files": [
|
||||
"aFTU7PB1QTsUX8KYthSQBK6PYK3EXw.woff2",
|
||||
"aFTU7PB1QTsUX8KYthqQBK6PYK0.woff2",
|
||||
"aFTR7PB1QTsUX8KYvumzEY2tbYf-Vlh3uA.woff2",
|
||||
"aFTR7PB1QTsUX8KYvumzEYOtbYf-Vlg.woff2"
|
||||
]
|
||||
},
|
||||
"fira-code": {
|
||||
"prefix": "https://fonts.gstatic.com/s/firacode/v27",
|
||||
"files": [
|
||||
"uU9NCBsR6Z2vfE9aq3bh0NSDqFGedCMX.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bh2dSDqFGedCMX.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bh0dSDqFGedCMX.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bh3tSDqFGedCMX.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bhZ_Wmh3mUfBsu_Q.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bh09SDqFGedCMX.woff2",
|
||||
"uU9NCBsR6Z2vfE9aq3bh3dSDqFGedA.woff2"
|
||||
]
|
||||
},
|
||||
"inconsolata": {
|
||||
"prefix": "https://fonts.gstatic.com/s/inconsolata/v37",
|
||||
"files": [
|
||||
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyxq15IDhunJ_o.woff2",
|
||||
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyx615IDhunJ_o.woff2",
|
||||
"QlddNThLqRwH-OJ1UHjlKENVzkWGVkL3GZQmAwLyya15IDhunA.woff2"
|
||||
]
|
||||
},
|
||||
"jetbrains-mono": {
|
||||
"prefix": "https://fonts.gstatic.com/s/jetbrainsmono/v24",
|
||||
"files": [
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwgknk-6nFg.woff2",
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwgknk-6nFg.woff2",
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxPcwgknk-6nFg.woff2",
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx_cwgknk-6nFg.woff2",
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx7cwgknk-6nFg.woff2",
|
||||
"tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwgknk-4.woff2"
|
||||
]
|
||||
},
|
||||
"roboto-mono": {
|
||||
"prefix": "https://fonts.gstatic.com/s/robotomono/v31",
|
||||
"files": [
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2",
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhPq3-cXbKDO1w.woff2",
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhIq3-cXbKDO1w.woff2",
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhEq3-cXbKDO1w.woff2",
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhFq3-cXbKDO1w.woff2",
|
||||
"L0x5DF4xlVMF-BfR8bXMIjhLq3-cXbKD.woff2"
|
||||
]
|
||||
},
|
||||
"source-code-pro": {
|
||||
"prefix": "https://fonts.gstatic.com/s/sourcecodepro/v31",
|
||||
"files": [
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMOvWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlOevWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMevWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPuvWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlMuvWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlM-vWnsUnxlC9.woff2",
|
||||
"HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevWnsUnxg.woff2"
|
||||
]
|
||||
},
|
||||
"space-mono": {
|
||||
"prefix": "https://fonts.gstatic.com/s/spacemono/v17",
|
||||
"files": [
|
||||
"i7dPIFZifjKcF5UAWdDRYE58RXi4EwSsbg.woff2",
|
||||
"i7dPIFZifjKcF5UAWdDRYE98RXi4EwSsbg.woff2",
|
||||
"i7dPIFZifjKcF5UAWdDRYEF8RXi4EwQ.woff2",
|
||||
"i7dMIFZifjKcF5UAWdDRaPpZUFqaHi6WZ3S_Yg.woff2",
|
||||
"i7dMIFZifjKcF5UAWdDRaPpZUFuaHi6WZ3S_Yg.woff2",
|
||||
"i7dMIFZifjKcF5UAWdDRaPpZUFWaHi6WZ3Q.woff2"
|
||||
]
|
||||
},
|
||||
"noto-color-emoji": {
|
||||
"prefix": "https://fonts.gstatic.com/s/notocoloremoji/v40",
|
||||
"files": [
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.0.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.1.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.2.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.3.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.4.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.5.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.6.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.7.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.8.woff2",
|
||||
"Yq6P-KqIXTD0t4D9z1ESnKM3-HpFabts6diysYTngZPnMC1MfLd4gw.9.woff2"
|
||||
]
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"abcfavorit/7bb31c73cb8d820e.woff2": "./ABCFavorit/ABCFavorit-Variable.woff2",
|
||||
"abcfavorit/aed849de6e333605.woff2": "./ABCFavorit/ABCFavorit-BoldItalic.woff2",
|
||||
"abcfavorit/b435fb7330f5e171.woff2": "./ABCFavorit/ABCFavorit-MediumItalic.woff2",
|
||||
"abcfavorit/aee27c224ccd5167.woff2": "./ABCFavorit/ABCFavorit-RegularItalic.woff2"
|
||||
}
|
||||
}
|
||||
@@ -5,32 +5,27 @@ import type {
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { generateFontFacesCSS, getFontSourcesToPreload } from './custom';
|
||||
import { generateDefaultFontFacesCSS } from './default';
|
||||
|
||||
export { DEFAULT_MONOSPACE_FONT, generateEmojiFontFacesCSS } from './default';
|
||||
import { fonts } from './default';
|
||||
|
||||
/**
|
||||
* Represents font data for either a default font or a custom font
|
||||
*/
|
||||
export type FontData = DefaultFontData | CustomFontData;
|
||||
|
||||
interface BaseFontData {
|
||||
/** `@font-face` rules and the CSS variable, to inline in the document head. */
|
||||
fontFaceRules: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font data for a default font, self-hosted from our own assets
|
||||
* Font data for a default font, currently handle with next/font
|
||||
*/
|
||||
interface DefaultFontData extends BaseFontData {
|
||||
interface DefaultFontData {
|
||||
type: 'default';
|
||||
variable: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font data for a custom font with @font-face rules
|
||||
*/
|
||||
interface CustomFontData extends BaseFontData {
|
||||
interface CustomFontData {
|
||||
type: 'custom';
|
||||
fontFaceRules: string;
|
||||
preloadSources: CustomizationFontDefinition['fontFaces'];
|
||||
}
|
||||
|
||||
@@ -44,7 +39,7 @@ export function getFontData(
|
||||
if (typeof font === 'string') {
|
||||
return {
|
||||
type: 'default',
|
||||
fontFaceRules: generateDefaultFontFacesCSS(font),
|
||||
variable: fonts[font].variable,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
export interface FontVariantData {
|
||||
weight: string;
|
||||
style: string;
|
||||
/** Path under `~gitbook/static/fonts`, index-aligned with the family's `subsets`. */
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export interface FontFallbackFaceData {
|
||||
family: string;
|
||||
local: string;
|
||||
ascentOverride: string;
|
||||
descentOverride: string;
|
||||
lineGapOverride: string;
|
||||
sizeAdjust: string;
|
||||
}
|
||||
|
||||
export interface FontFamilyData {
|
||||
family: string;
|
||||
variable: string;
|
||||
/** Full value for the CSS variable, fallbacks included. */
|
||||
fontFamilyValue: string;
|
||||
/** `unicode-range` per subset. Held once per family rather than repeated on every variant. */
|
||||
subsets: string[];
|
||||
variants: FontVariantData[];
|
||||
fallbackFace: FontFallbackFaceData | null;
|
||||
/** Applied to every face of the family. */
|
||||
ascentOverride?: string;
|
||||
}
|
||||
|
||||
export type FontFacesData = Record<string, FontFamilyData>;
|
||||
|
||||
/** Where `scripts/download-fonts.ts` fetches (or copies) each file from. */
|
||||
export interface FontSourcesData {
|
||||
google: Record<string, { prefix: string; files: string[] }>;
|
||||
local: Record<string, string>;
|
||||
}
|
||||
@@ -5,9 +5,7 @@
|
||||
"outputs": [
|
||||
"public/~gitbook/static/icons/**/*",
|
||||
"public/~gitbook/static/math/**/*",
|
||||
"public/~gitbook/static/embed/**/*",
|
||||
"public/~gitbook/static/css/**/*",
|
||||
"public/~gitbook/static/fonts/**/*"
|
||||
"public/~gitbook/static/embed/**/*"
|
||||
],
|
||||
"dependsOn": ["^generate", "@gitbook/embed#build"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user