From feb6735a4dd58cac05741b67488739617f708f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Wed, 26 Feb 2025 13:20:16 +0100 Subject: [PATCH] Abstract data fetching between v1 and v2 (#2865) --- .github/workflows/ci.yaml | 18 +- .prettierignore | 5 +- bun.lock | 169 ++++--- packages/cache-do/package.json | 2 +- packages/gitbook-v2/.gitignore | 3 + packages/gitbook-v2/README.md | 104 ---- packages/gitbook-v2/next.config.mjs | 22 +- packages/gitbook-v2/package.json | 15 +- packages/gitbook-v2/postcss.config.js | 6 + .../src/app/dynamic/url/[...url]/layout.tsx | 15 - .../src/app/dynamic/url/[...url]/page.tsx | 9 - .../[mode]/[siteURL]/[pagePath]/loading.tsx | 5 + .../[mode]/[siteURL]/[pagePath]/not-found.tsx | 5 + .../[mode]/[siteURL]/[pagePath]/page.tsx | 38 ++ .../sites/dynamic/[mode]/[siteURL]/layout.tsx | 28 ++ .../[mode]/[siteURL]/[pagePath]/loading.tsx | 5 + .../[mode]/[siteURL]/[pagePath]/not-found.tsx | 5 + .../[mode]/[siteURL]/[pagePath]/page.tsx | 44 ++ .../sites/static/[mode]/[siteURL]/layout.tsx | 28 ++ .../src/app/static/url/[...url]/layout.tsx | 21 - .../src/app/static/url/[...url]/page.tsx | 15 - packages/gitbook-v2/src/app/utils.ts | 116 +++++ .../components/routes/SiteContentLayout.tsx | 27 - .../src/components/routes/SiteContentPage.tsx | 12 - packages/gitbook-v2/src/lib/cache.ts | 33 +- packages/gitbook-v2/src/lib/context.ts | 339 ++++++++++--- packages/gitbook-v2/src/lib/data/api.ts | 465 ++++++++++++++++++ packages/gitbook-v2/src/lib/data/index.ts | 4 + packages/gitbook-v2/src/lib/data/types.ts | 124 +++++ .../src/lib/data/urls.test.ts} | 2 +- packages/gitbook-v2/src/lib/data/urls.ts | 252 ++++++++++ packages/gitbook-v2/src/lib/data/utils.ts | 19 + packages/gitbook-v2/src/lib/env.ts | 23 + .../src/lib/images/createImageResizer.ts} | 159 +++--- packages/gitbook-v2/src/lib/images/index.ts | 4 + .../src/lib/images/signatures.ts} | 55 ++- packages/gitbook-v2/src/lib/images/types.ts | 29 ++ packages/gitbook-v2/src/lib/images/utils.ts | 17 + packages/gitbook-v2/src/lib/links.ts | 91 ++++ packages/gitbook-v2/src/lib/middleware.ts | 36 ++ packages/gitbook-v2/src/middleware.ts | 104 +++- packages/gitbook-v2/tailwind.config.ts | 10 + packages/gitbook-v2/tsconfig.json | 28 +- packages/gitbook/e2e/util.ts | 2 + packages/gitbook/package.json | 2 +- .../src/app/(global)/~gitbook/image/route.ts | 21 +- .../(content)/[[...pathname]]/loading.tsx | 24 +- .../(content)/[[...pathname]]/not-found.tsx | 38 +- .../(site)/(content)/[[...pathname]]/page.tsx | 223 ++------- .../middleware/(site)/(content)/layout.tsx | 161 +----- .../(site)/(core)/llms.txt/route.ts | 2 +- .../(site)/(core)/robots.txt/route.ts | 11 +- .../(site)/(core)/sitemap.xml/route.ts | 2 +- .../(site)/(core)/~gitbook/icon/route.tsx | 12 +- .../~gitbook/ogimage/[pageId]/route.tsx | 17 +- .../src/app/middleware/(site)/fetch.ts | 143 ------ .../src/app/middleware/(site)/layout.tsx | 4 +- .../(space)/~gitbook/pdf/layout.tsx | 34 +- .../middleware/(space)/~gitbook/pdf/page.tsx | 102 ++-- .../(space)/~gitbook/pdf/pointer.ts | 14 + .../components/AdminToolbar/AdminToolbar.tsx | 41 +- .../src/components/Ads/AdClassicRendering.tsx | 13 +- .../src/components/Ads/AdCoverRendering.tsx | 10 +- .../gitbook/src/components/Ads/renderAd.tsx | 9 +- .../src/components/DocumentView/Block.tsx | 12 +- .../DocumentView/BlockContentRef.tsx | 34 +- .../DocumentView/CodeBlock/PlainCodeBlock.tsx | 2 - .../components/DocumentView/DocumentView.tsx | 19 +- .../src/components/DocumentView/Drawing.tsx | 8 +- .../src/components/DocumentView/Embed.tsx | 4 +- .../src/components/DocumentView/File.tsx | 5 +- .../src/components/DocumentView/Images.tsx | 8 +- .../components/DocumentView/InlineImage.tsx | 21 +- .../components/DocumentView/InlineLink.tsx | 6 +- .../Integration/IntegrationBlock.tsx | 4 +- .../src/components/DocumentView/Mention.tsx | 9 +- .../DocumentView/OpenAPI/OpenAPI.tsx | 6 +- .../DocumentView/ReusableContent.tsx | 11 +- .../DocumentView/Table/RecordCard.tsx | 14 +- .../DocumentView/Table/RecordColumnValue.tsx | 32 +- .../gitbook/src/components/Footer/Footer.tsx | 13 +- .../components/Footer/FooterLinksGroup.tsx | 7 +- .../gitbook/src/components/Header/Header.tsx | 36 +- .../src/components/Header/HeaderLink.tsx | 13 +- .../src/components/Header/HeaderLinkMore.tsx | 13 +- .../src/components/Header/HeaderLogo.tsx | 28 +- .../src/components/Header/SpacesDropdown.tsx | 24 +- .../components/Insights/InsightsProvider.tsx | 16 +- .../Insights/TrackPageViewEvent.tsx | 9 +- .../src/components/PageAside/PageAside.tsx | 18 +- .../src/components/PageBody/PageBody.tsx | 38 +- .../PageBody/PageBodyBlankslate.tsx | 18 +- .../src/components/PageBody/PageCover.tsx | 7 +- .../src/components/Search/server-actions.tsx | 94 ++-- .../SiteLayout}/ClientContexts.tsx | 4 +- .../SiteLayout}/RocketLoaderDetector.tsx | 2 +- .../src/components/SiteLayout/SiteLayout.tsx | 121 +++++ .../src/components/SiteLayout/index.ts | 1 + .../components/SitePage/PageClientLayout.tsx | 40 ++ .../src/components/SitePage/SitePage.tsx | 191 +++++++ .../components/SitePage/SitePageNotFound.tsx | 36 ++ .../components/SitePage/SitePageSkeleton.tsx | 28 ++ .../gitbook/src/components/SitePage/fetch.ts | 103 ++++ .../gitbook/src/components/SitePage/index.ts | 4 + .../components/SpaceLayout/SpaceLayout.tsx | 118 ++--- .../TableOfContents/PageDocumentItem.tsx | 17 +- .../TableOfContents/PageGroupItem.tsx | 14 +- .../TableOfContents/PageLinkItem.tsx | 5 +- .../components/TableOfContents/PagesList.tsx | 16 +- .../TableOfContents/TableOfContents.tsx | 25 +- .../TableOfContents/ToggleableLinkItem.tsx | 12 +- .../gitbook/src/components/hooks/index.ts | 1 + .../components/hooks/useCurrentPagePath.ts | 22 + .../gitbook/src/components/utils/Image.tsx | 22 +- packages/gitbook/src/lib/api.ts | 236 +-------- packages/gitbook/src/lib/async.ts | 13 + packages/gitbook/src/lib/customization.ts | 42 ++ packages/gitbook/src/lib/document-sections.ts | 8 +- packages/gitbook/src/lib/middleware.ts | 122 ----- packages/gitbook/src/lib/openapi/fetch.ts | 7 +- packages/gitbook/src/lib/pages.ts | 1 - packages/gitbook/src/lib/paths.ts | 17 + packages/gitbook/src/lib/pointer.ts | 2 +- packages/gitbook/src/lib/references.tsx | 234 +++------ packages/gitbook/src/lib/seo.ts | 16 +- packages/gitbook/src/lib/sites.ts | 66 +++ packages/gitbook/src/lib/urls.ts | 18 - packages/gitbook/src/lib/utils.ts | 27 - packages/gitbook/src/lib/v1.ts | 193 ++++++++ packages/gitbook/src/lib/visitor-token.ts | 9 - packages/gitbook/src/middleware.ts | 43 +- packages/gitbook/tsconfig.json | 3 +- packages/react-contentkit/package.json | 2 +- packages/react-openapi/package.json | 2 +- 134 files changed, 3573 insertions(+), 2195 deletions(-) create mode 100644 packages/gitbook-v2/postcss.config.js delete mode 100644 packages/gitbook-v2/src/app/dynamic/url/[...url]/layout.tsx delete mode 100644 packages/gitbook-v2/src/app/dynamic/url/[...url]/page.tsx create mode 100644 packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/loading.tsx create mode 100644 packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/not-found.tsx create mode 100644 packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/page.tsx create mode 100644 packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/loading.tsx create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx create mode 100644 packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx delete mode 100644 packages/gitbook-v2/src/app/static/url/[...url]/layout.tsx delete mode 100644 packages/gitbook-v2/src/app/static/url/[...url]/page.tsx create mode 100644 packages/gitbook-v2/src/app/utils.ts delete mode 100644 packages/gitbook-v2/src/components/routes/SiteContentLayout.tsx delete mode 100644 packages/gitbook-v2/src/components/routes/SiteContentPage.tsx create mode 100644 packages/gitbook-v2/src/lib/data/api.ts create mode 100644 packages/gitbook-v2/src/lib/data/index.ts create mode 100644 packages/gitbook-v2/src/lib/data/types.ts rename packages/{gitbook/src/lib/middleware.test.ts => gitbook-v2/src/lib/data/urls.test.ts} (99%) create mode 100644 packages/gitbook-v2/src/lib/data/urls.ts create mode 100644 packages/gitbook-v2/src/lib/data/utils.ts rename packages/{gitbook/src/lib/images.ts => gitbook-v2/src/lib/images/createImageResizer.ts} (61%) create mode 100644 packages/gitbook-v2/src/lib/images/index.ts rename packages/{gitbook/src/lib/image-signatures.ts => gitbook-v2/src/lib/images/signatures.ts} (76%) create mode 100644 packages/gitbook-v2/src/lib/images/types.ts create mode 100644 packages/gitbook-v2/src/lib/images/utils.ts create mode 100644 packages/gitbook-v2/src/lib/links.ts create mode 100644 packages/gitbook-v2/src/lib/middleware.ts create mode 100644 packages/gitbook-v2/tailwind.config.ts delete mode 100644 packages/gitbook/src/app/middleware/(site)/fetch.ts rename packages/gitbook/src/{app/middleware/(site)/(content) => components/SiteLayout}/ClientContexts.tsx (89%) rename packages/gitbook/src/{app/middleware/(site)/(content) => components/SiteLayout}/RocketLoaderDetector.tsx (95%) create mode 100644 packages/gitbook/src/components/SiteLayout/SiteLayout.tsx create mode 100644 packages/gitbook/src/components/SiteLayout/index.ts create mode 100644 packages/gitbook/src/components/SitePage/PageClientLayout.tsx create mode 100644 packages/gitbook/src/components/SitePage/SitePage.tsx create mode 100644 packages/gitbook/src/components/SitePage/SitePageNotFound.tsx create mode 100644 packages/gitbook/src/components/SitePage/SitePageSkeleton.tsx create mode 100644 packages/gitbook/src/components/SitePage/fetch.ts create mode 100644 packages/gitbook/src/components/SitePage/index.ts create mode 100644 packages/gitbook/src/components/hooks/useCurrentPagePath.ts create mode 100644 packages/gitbook/src/lib/customization.ts create mode 100644 packages/gitbook/src/lib/sites.ts create mode 100644 packages/gitbook/src/lib/v1.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1548a6ba4..2fbdcca02 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,7 @@ jobs: echo "SENTRY_ENVIRONMENT=preview" >> $GITHUB_ENV if: 1 && !startsWith(github.ref, 'refs/heads/main') - name: Build Next.js with next-on-pages - run: bun run turbo build:cloudflare + run: bun run turbo gitbook#build:cloudflare env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ secrets.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY }} @@ -49,7 +49,7 @@ jobs: SENTRY_RELEASE: ${{ github.sha }} - id: deploy name: Deploy to Cloudflare - uses: cloudflare/wrangler-action@v3.11.0 + uses: cloudflare/wrangler-action@v3.14.0 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -103,7 +103,8 @@ jobs: run: | echo "VERCEL_ENVIRONMENT=production" >> $GITHUB_ENV echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV - echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV + echo "GITBOOK_URL=https://open-2v.gitbook.com" >> $GITHUB_ENV + echo "GITBOOK_ASSETS_PREFIX=https://static-2v.gitbook.com" >> $GITHUB_ENV if: startsWith(github.ref, 'refs/heads/main') - name: Sets env vars for preview run: | @@ -124,7 +125,7 @@ jobs: - name: Deploy Project Artifacts to Vercel id: deploy run: | - DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}) + DEPLOYMENT_URL=$(bun run vercel deploy --prebuilt --target=$VERCEL_ENVIRONMENT $([ "$VERCEL_ENVIRONMENT" = "production" ] && echo "--prod") --token=${{ secrets.VERCEL_TOKEN }}) echo "deployment-url=$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" - name: Outputs run: | @@ -156,7 +157,8 @@ jobs: - name: Sets env vars for production run: | echo "SENTRY_ENVIRONMENT=production" >> $GITHUB_ENV - echo "GITBOOK_ASSETS_PREFIX=https://static.gitbook.com" >> $GITHUB_ENV + echo "GITBOOK_URL=https://open-2c.gitbook.com" >> $GITHUB_ENV + echo "GITBOOK_ASSETS_PREFIX=https://static-2c.gitbook.com" >> $GITHUB_ENV if: startsWith(github.ref, 'refs/heads/main') - name: Sets env vars for preview run: | @@ -173,7 +175,7 @@ jobs: SENTRY_RELEASE: ${{ github.sha }} - id: deploy name: Deploy to Cloudflare - uses: cloudflare/wrangler-action@v3.11.0 + uses: cloudflare/wrangler-action@v3.14.0 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} @@ -226,8 +228,8 @@ jobs: | Site | v1 | v2 | | --- | --- | --- | - | GitBook | [${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/docs.gitbook.com) | - | E2E | [${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/static/url/gitbook.gitbook.io/test-gitbook-open) | + | GitBook | [${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com](${{ needs.deploy.outputs.deployment-url }}/docs.gitbook.com) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/docs.gitbook.com) | + | E2E | [${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy.outputs.deployment-url }}/gitbook.gitbook.io/test-gitbook-open) | [${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open](${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/gitbook.gitbook.io/test-gitbook-open) | edit-mode: replace visual-testing: runs-on: ubuntu-latest diff --git a/.prettierignore b/.prettierignore index 3413402d8..aeeb02c9a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,4 +11,7 @@ packages/icons/src/data/*.json dist/ README.md -CHANGELOG.md \ No newline at end of file +CHANGELOG.md + +# v2 +packages/gitbook-v2/public/~gitbook/static/ \ No newline at end of file diff --git a/bun.lock b/bun.lock index 71151a698..584da905b 100644 --- a/bun.lock +++ b/bun.lock @@ -19,7 +19,7 @@ }, "devDependencies": { "typescript": "^5.5.3", - "wrangler": "3.82.0", + "wrangler": "^3.109.2", }, }, "packages/colors": { @@ -40,7 +40,7 @@ "name": "gitbook", "version": "0.6.2", "dependencies": { - "@gitbook/api": "^0.95.0", + "@gitbook/api": "^0.96.0", "@gitbook/cache-do": "workspace:*", "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", @@ -124,20 +124,25 @@ "name": "gitbook-v2", "version": "0.1.0", "dependencies": { - "@gitbook/api": "^0.93.0", + "@gitbook/api": "^0.96.0", + "@sindresorhus/fnv1a": "^3.1.0", "next": "canary", "react": "^19.0.0", "react-dom": "^19.0.0", + "server-only": "^0.0.1", }, "devDependencies": { - "@opennextjs/cloudflare": "^0.4.3", + "@opennextjs/cloudflare": "^0.5.3", + "gitbook": "*", + "postcss": "^8", + "tailwindcss": "^3.4.0", }, }, "packages/icons": { "name": "@gitbook/icons", "version": "0.1.0", "bin": { - "gitbook-icons": "./bin/gitbook-icons.js" + "gitbook-icons": "./bin/gitbook-icons.js", }, "dependencies": { "@fortawesome/fontawesome-free": "^6.6.0", @@ -179,7 +184,7 @@ "name": "@gitbook/react-contentkit", "version": "0.6.0", "dependencies": { - "@gitbook/api": "^0.93.0", + "@gitbook/api": "^0.96.0", "assert-never": "^1.2.1", "classnames": "^2.5.1", }, @@ -194,7 +199,7 @@ "name": "@gitbook/react-math", "version": "0.6.0", "bin": { - "gitbook-math": "./bin/gitbook-math.js" + "gitbook-math": "./bin/gitbook-math.js", }, "dependencies": { "object-hash": "^3.0.0", @@ -447,17 +452,15 @@ "@cloudflare/next-on-pages": ["@cloudflare/next-on-pages@1.13.7", "", { "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", "chalk": "^5.2.0", "chokidar": "^3.5.3", "commander": "^11.1.0", "cookie": "^0.5.0", "esbuild": "^0.15.3", "js-yaml": "^4.1.0", "miniflare": "^3.20231218.1", "package-manager-manager": "^0.2.0", "pcre-to-regexp": "^1.1.0", "semver": "^7.5.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20240208.0", "vercel": ">=30.0.0", "wrangler": "^3.28.2" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "next-on-pages": "bin/index.js" } }, "sha512-TSMVy+1fmxzeyykOC9guMEj7G2FgENw1T8V1sIFnah6piaJNBmybdyikPdQSdikP5w6v9eQhBt/TrXDPMDw0dw=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250214.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cDvvedWDc5zrgDnuXe2qYcz/TwBvzmweO55C7XpPuAWJ9Oqxv81PkdekYxD8mH989aQ/GI5YD0Fe6fDYlM+T3Q=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250214.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NytCvRveVzu0mRKo+tvZo3d/gCUway3B2ZVqSi/TS6NXDGBYIJo7g6s3BnTLS74kgyzeDOjhu9j/RBJBS809qw=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250214.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pQ7+aHNHj8SiYEs4d/6cNoimE5xGeCMfgU1yfDFtA9YGN9Aj2BITZgOWPec+HW7ZkOy9oWlNrO6EvVjGgB4tbQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250214.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Vhlfah6Yd9ny1npNQjNgElLIjR6OFdEbuR3LCfbLDCwzWEBFhIf7yC+Tpp/a0Hq7kLz3sLdktaP7xl3PJhyOjA=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="], - - "@cloudflare/workers-shared": ["@cloudflare/workers-shared@0.6.0", "", { "dependencies": { "mime": "^3.0.0", "zod": "^3.22.3" } }, "sha512-rfUCvb3hx4AsvdUZsxgk9lmgEnQehqV3jdtXLP/Xr0+P56n11T/0nXNMzmn7Nnv+IJFOV6X9NmFhuMz4sBPw7w=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250214.0", "", { "os": "win32", "cpu": "x64" }, "sha512-GMwMyFbkjBKjYJoKDhGX8nuL4Gqe3IbVnVWf2Q6086CValyIknupk5J6uQWGw2EBU3RGO3x4trDXT5WphQJZDQ=="], "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20241230.0", "", {}, "sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ=="], @@ -595,7 +598,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.93.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-HOOEiKTtmkTpnaOj2OfV5dtvNN+R+z9kbAJSqh7LvaI86iMo/KvRHUa9vCq7gdrJhCLaeF0D5DAAYZZzK1Yz5Q=="], + "@gitbook/api": ["@gitbook/api@0.96.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-JN6k0PlpyQQhtr8sNnbKIWQ7n9gCFqhrDo33v/41d41M1wgz+eowm1jdiYtX0tsXBH7D1YHmBMbNnDI8jQwbwA=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], @@ -767,9 +770,9 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@opennextjs/aws": ["@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@715", { "dependencies": { "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@esbuild-plugins/node-resolve": "0.2.2", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "promise.series": "^0.2.0", "urlpattern-polyfill": "^10.0.0" }, "bin": { "open-next": "./dist/index.js" } }], + "@opennextjs/aws": ["@opennextjs/aws@https://pkg.pr.new/@opennextjs/aws@733", { "dependencies": { "@aws-sdk/client-cloudfront": "3.398.0", "@aws-sdk/client-dynamodb": "^3.398.0", "@aws-sdk/client-lambda": "^3.398.0", "@aws-sdk/client-s3": "^3.398.0", "@aws-sdk/client-sqs": "^3.398.0", "@esbuild-plugins/node-resolve": "0.2.2", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.1", "aws4fetch": "^1.0.18", "chalk": "^5.3.0", "esbuild": "0.19.2", "express": "5.0.1", "path-to-regexp": "^6.3.0", "promise.series": "^0.2.0", "urlpattern-polyfill": "^10.0.0" }, "bin": { "open-next": "./dist/index.js" } }], - "@opennextjs/cloudflare": ["@opennextjs/cloudflare@0.4.3", "", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@715", "enquirer": "^2.4.1", "glob": "^11.0.0", "ts-morph": "^23.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.107.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-DUgD+kMv/1UYVNtHALZJhmqQ2nAdqHQqPqNEGGLVvoxM/t8tdNAlERx5riM6artUoP2PWDYvBMk6DHJ6potNxQ=="], + "@opennextjs/cloudflare": ["@opennextjs/cloudflare@0.5.3", "", { "dependencies": { "@ast-grep/napi": "^0.34.1", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@733", "enquirer": "^2.4.1", "glob": "^11.0.0", "yaml": "^2.7.0" }, "peerDependencies": { "wrangler": "^3.107.3" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-0A8oXlRBnPHc0fhAfoFSOq2ZAuPC/n/Vj87lytcU4Cese2VsRqf89CxBMCSqAH9g+OB+kUZ2jOGGntGpVOvM0g=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -1335,7 +1338,7 @@ "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - "@ts-morph/common": ["@ts-morph/common@0.24.0", "", { "dependencies": { "fast-glob": "^3.3.2", "minimatch": "^9.0.4", "mkdirp": "^3.0.1", "path-browserify": "^1.0.1" } }, "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A=="], + "@ts-morph/common": ["@ts-morph/common@0.11.1", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], @@ -1385,8 +1388,6 @@ "@types/node": ["@types/node@20.16.11", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw=="], - "@types/node-forge": ["@types/node-forge@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ=="], - "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], "@types/object-hash": ["@types/object-hash@3.0.6", "", {}, "sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w=="], @@ -1553,7 +1554,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], @@ -1723,7 +1724,7 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + "code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="], "codemirror": ["codemirror@6.0.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg=="], @@ -1745,6 +1746,8 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "configstore": ["configstore@5.0.1", "", { "dependencies": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", "make-dir": "^3.0.0", "unique-string": "^2.0.0", "write-file-atomic": "^3.0.0", "xdg-basedir": "^4.0.0" } }, "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA=="], "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], @@ -1795,8 +1798,6 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.0", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA=="], - "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -2365,8 +2366,6 @@ "iterator.prototype": ["iterator.prototype@1.1.3", "", { "dependencies": { "define-properties": "^1.2.1", "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", "reflect.getprototypeof": "^1.0.4", "set-function-name": "^2.0.1" } }, "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ=="], - "itty-time": ["itty-time@1.0.6", "", {}, "sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw=="], - "jackspeak": ["jackspeak@4.0.2", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw=="], "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], @@ -2605,7 +2604,7 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="], + "miniflare": ["miniflare@3.20250214.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250214.0", "ws": "8.18.0", "youch": "3.2.3", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-XKwn+X/V2CEpbRhoeaIcJHpV/Duz5Md5rxVT8I6S1oqd3aLZkn8cUX1tuxHpUvfQSPuXwWH+2ESLNnTf9PKEWg=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -2621,6 +2620,8 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + "mnemonist": ["mnemonist@0.38.3", "", { "dependencies": { "obliterator": "^1.6.1" } }, "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw=="], "module-details-from-path": ["module-details-from-path@1.0.3", "", {}, "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="], @@ -2633,7 +2634,7 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + "nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -2649,7 +2650,7 @@ "node-fetch": ["node-fetch@2.6.7", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ=="], - "node-forge": ["node-forge@1.3.1", "", {}, "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA=="], + "node-forge": ["node-forge@0.10.0", "", {}, "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="], "node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="], @@ -2789,6 +2790,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "playwright": ["playwright@1.49.1", "", { "dependencies": { "playwright-core": "1.49.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA=="], "playwright-core": ["playwright-core@1.49.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg=="], @@ -2947,8 +2950,6 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "resolve.exports": ["resolve.exports@2.0.2", "", {}, "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg=="], - "responselike": ["responselike@1.0.2", "", { "dependencies": { "lowercase-keys": "^1.0.0" } }, "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ=="], "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], @@ -2983,8 +2984,6 @@ "schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="], - "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], "semver-diff": ["semver-diff@3.1.1", "", { "dependencies": { "semver": "^6.3.0" } }, "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg=="], @@ -3171,7 +3170,7 @@ "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], - "ts-morph": ["ts-morph@23.0.0", "", { "dependencies": { "@ts-morph/common": "~0.24.0", "code-block-writer": "^13.0.1" } }, "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug=="], + "ts-morph": ["ts-morph@12.0.0", "", { "dependencies": { "@ts-morph/common": "~0.11.0", "code-block-writer": "^10.1.1" } }, "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA=="], "ts-node": ["ts-node@10.9.1", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-script": "dist/bin-script-deprecated.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js" } }, "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw=="], @@ -3225,7 +3224,7 @@ "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "unenv": ["unenv-nightly@2.0.0-20241018-011344-e666fcf", "", { "dependencies": { "defu": "^6.1.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-D00bYn8rzkCBOlLx+k1iHQlc69jvtJRT7Eek4yIGQ6461a2tUBjngGZdRpqsoXAJCz/qBW0NgPting7Zvg+ysg=="], + "unenv": ["unenv@2.0.0-rc.1", "", { "dependencies": { "defu": "^6.1.4", "mlly": "^1.7.4", "ohash": "^1.1.4", "pathe": "^1.1.2", "ufo": "^1.5.4" } }, "sha512-PU5fb40H8X149s117aB4ytbORcCvlASdtF97tfls4BPIyj4PeVxvpSuy1jAptqYHqB0vb2w2sHvzM0XWcp2OKg=="], "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], @@ -3333,9 +3332,9 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="], + "workerd": ["workerd@1.20250214.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250214.0", "@cloudflare/workerd-darwin-arm64": "1.20250214.0", "@cloudflare/workerd-linux-64": "1.20250214.0", "@cloudflare/workerd-linux-arm64": "1.20250214.0", "@cloudflare/workerd-windows-64": "1.20250214.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-QWcqXZLiMpV12wiaVnb3nLmfs/g4ZsFQq2mX85z546r3AX4CTIkXl0VP50W3CwqLADej3PGYiRDOTelDOwVG1g=="], - "wrangler": ["wrangler@3.82.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/workers-shared": "0.6.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "blake3-wasm": "^2.1.5", "chokidar": "^3.5.3", "date-fns": "^4.1.0", "esbuild": "0.17.19", "itty-time": "^1.0.6", "miniflare": "3.20241018.0", "nanoid": "^3.3.3", "path-to-regexp": "^6.3.0", "resolve": "^1.22.8", "resolve.exports": "^2.0.2", "selfsigned": "^2.0.1", "source-map": "^0.6.1", "unenv": "npm:unenv-nightly@2.0.0-20241018-011344-e666fcf", "workerd": "1.20241018.1", "xxhash-wasm": "^1.0.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20241018.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-W4gyIkxOfqbFU6QrUvK+Ay2rwH+DcyXmIuT/1/9EG9K6jngmmon8ifVYGRTftRb2Vu7hDNLovbGfjeAXm+CEXA=="], + "wrangler": ["wrangler@3.109.3", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250214.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.1", "workerd": "1.20250214.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250214.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-T+SfFYEbkWDzf8WRR1JzaVKPH7wAzyciRIua8qszT+UqPqFNb6M5beCHHK/de7Zk5z6W5lvLly9SHToC11maDQ=="], "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -3355,8 +3354,6 @@ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], - "xxhash-wasm": ["xxhash-wasm@1.0.2", "", {}, "sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A=="], - "y-codemirror.next": ["y-codemirror.next@0.3.5", "", { "dependencies": { "lib0": "^0.2.42" }, "peerDependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "yjs": "^13.5.6" } }, "sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -3377,7 +3374,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + "youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], @@ -4033,6 +4030,8 @@ "@cloudflare/next-on-pages/esbuild": ["esbuild@0.15.18", "", { "optionalDependencies": { "@esbuild/android-arm": "0.15.18", "@esbuild/linux-loong64": "0.15.18", "esbuild-android-64": "0.15.18", "esbuild-android-arm64": "0.15.18", "esbuild-darwin-64": "0.15.18", "esbuild-darwin-arm64": "0.15.18", "esbuild-freebsd-64": "0.15.18", "esbuild-freebsd-arm64": "0.15.18", "esbuild-linux-32": "0.15.18", "esbuild-linux-64": "0.15.18", "esbuild-linux-arm": "0.15.18", "esbuild-linux-arm64": "0.15.18", "esbuild-linux-mips64le": "0.15.18", "esbuild-linux-ppc64le": "0.15.18", "esbuild-linux-riscv64": "0.15.18", "esbuild-linux-s390x": "0.15.18", "esbuild-netbsd-64": "0.15.18", "esbuild-openbsd-64": "0.15.18", "esbuild-sunos-64": "0.15.18", "esbuild-windows-32": "0.15.18", "esbuild-windows-64": "0.15.18", "esbuild-windows-arm64": "0.15.18" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q=="], + "@cloudflare/next-on-pages/miniflare": ["miniflare@3.20241018.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "^8.8.0", "acorn-walk": "^8.2.0", "capnp-ts": "^0.7.0", "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", "undici": "^5.28.4", "workerd": "1.20241018.1", "ws": "^8.17.1", "youch": "^3.2.2", "zod": "^3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-g7i5oGAoJOk8+hJp77A5/wAdu7PEvi5hQc+0wzwzjhUNM2I5DHd2Cc29ACPhAe1kIXvCCVkxs3+REF52qnX0aw=="], + "@codemirror/lang-html/@codemirror/autocomplete": ["@codemirror/autocomplete@6.18.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA=="], "@codemirror/lang-html/@codemirror/lang-css": ["@codemirror/lang-css@6.3.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA=="], @@ -4225,8 +4224,6 @@ "@scalar/import/@scalar/openapi-parser": ["@scalar/openapi-parser@0.8.9", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-vTXrkl/hX3CG2dMe8mYutjsjfFLZOXGFJGqz6k8lUk8avFP/5eGEsd/eZe1UIcwtDwcjgJIyQ4p3pIC7jBvfuw=="], - "@scalar/oas-utils/nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="], - "@scalar/oas-utils/yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], "@scalar/use-toasts/nanoid": ["nanoid@5.0.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ=="], @@ -4413,11 +4410,7 @@ "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - "@ts-morph/common/fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "@ts-morph/common/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@ts-morph/common/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "@types/react-dom/@types/react": ["@types/react@18.3.11", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ=="], @@ -4441,8 +4434,6 @@ "@vercel/gatsby-plugin-vercel-builder/fs-extra": ["fs-extra@11.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw=="], - "@vercel/hydrogen/ts-morph": ["ts-morph@12.0.0", "", { "dependencies": { "@ts-morph/common": "~0.11.0", "code-block-writer": "^10.1.1" } }, "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA=="], - "@vercel/nft/@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@vercel/nft/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], @@ -4595,9 +4586,7 @@ "gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "gitbook/@gitbook/api": ["@gitbook/api@0.95.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9KAbt27Ile6cqAch7QEbiJHALQHojYlhsPzilgdQ5wpHgLwsrd7Smd58A3/8bWBKq4KV0vP4rh3oYhIw+LlWFw=="], - - "gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="], + "gitbook-v2/next": ["next@15.2.0-canary.74", "", { "dependencies": { "@next/env": "15.2.0-canary.74", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.74", "@next/swc-darwin-x64": "15.2.0-canary.74", "@next/swc-linux-arm64-gnu": "15.2.0-canary.74", "@next/swc-linux-arm64-musl": "15.2.0-canary.74", "@next/swc-linux-x64-gnu": "15.2.0-canary.74", "@next/swc-linux-x64-musl": "15.2.0-canary.74", "@next/swc-win32-arm64-msvc": "15.2.0-canary.74", "@next/swc-win32-x64-msvc": "15.2.0-canary.74", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-0BkYq0136wVKLogN0R85cnSKpnlSHg4/mA5lMWJGp/ltfai+vRaejWre1fhpjNVFuNOnzblFhpTQlwxZk/mN8g=="], "glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="], @@ -4605,8 +4594,6 @@ "google-auth-library/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "google-p12-pem/node-forge": ["node-forge@0.10.0", "", {}, "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA=="], - "googleapis-common/uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], "got/get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="], @@ -4653,10 +4640,20 @@ "micro/content-type": ["content-type@1.0.4", "", {}, "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="], + "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "miniflare/undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], + + "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + "minimist-options/arrify": ["arrify@1.0.1", "", {}, "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA=="], "minizlib/minipass": ["minipass@2.9.0", "", { "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg=="], + "mlly/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "next/@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -4675,8 +4672,12 @@ "path-scurry/lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="], + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + "postcss-load-config/lilconfig": ["lilconfig@3.1.2", "", {}, "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow=="], "psi/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="], @@ -4759,6 +4760,8 @@ "terser-webpack-plugin/schema-utils": ["schema-utils@4.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g=="], + "ts-node/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + "ts-node/arg": ["arg@4.1.0", "", {}, "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="], "type-is/mime-types": ["mime-types@3.0.0", "", { "dependencies": { "mime-db": "^1.53.0" } }, "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w=="], @@ -4777,8 +4780,6 @@ "which-builtin-type/isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], - "wrangler/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -4787,8 +4788,6 @@ "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "@argos-ci/core/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw=="], @@ -5245,6 +5244,12 @@ "@cloudflare/next-on-pages/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.15.18", "", { "os": "linux", "cpu": "none" }, "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ=="], + "@cloudflare/next-on-pages/miniflare/acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "@cloudflare/next-on-pages/miniflare/workerd": ["workerd@1.20241018.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20241018.1", "@cloudflare/workerd-darwin-arm64": "1.20241018.1", "@cloudflare/workerd-linux-64": "1.20241018.1", "@cloudflare/workerd-linux-arm64": "1.20241018.1", "@cloudflare/workerd-windows-64": "1.20241018.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-JPW2oAbYOnJj1c5boyDOdjl/Yvur45jhVE8lf+I9oxR6myyAvuH2tdXO62kye68jRluJOMUeyssLes+JRwLmaA=="], + + "@cloudflare/next-on-pages/miniflare/youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + "@codemirror/lang-json/@codemirror/language/@codemirror/view": ["@codemirror/view@6.34.1", "", { "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ=="], "@codemirror/lang-json/@codemirror/language/@lezer/common": ["@lezer/common@1.2.2", "", {}, "sha512-Z+R3hN6kXbgBWAuejUNPihylAL1Z5CaFqnIe0nTX8Ej+XlIy3EGtXxn6WtLMO+os2hRkQvm2yvaGMYliUzlJaw=="], @@ -5383,10 +5388,6 @@ "@smithy/util-endpoints/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw=="], - "@ts-morph/common/fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "@vercel/fun/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], @@ -5435,10 +5436,6 @@ "@vercel/gatsby-plugin-vercel-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "@vercel/hydrogen/ts-morph/@ts-morph/common": ["@ts-morph/common@0.11.1", "", { "dependencies": { "fast-glob": "^3.2.7", "minimatch": "^3.0.4", "mkdirp": "^1.0.4", "path-browserify": "^1.0.1" } }, "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g=="], - - "@vercel/hydrogen/ts-morph/code-block-writer": ["code-block-writer@10.1.1", "", {}, "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw=="], - "@vercel/nft/glob/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "@vercel/node/esbuild/esbuild-android-64": ["esbuild-android-64@0.14.47", "", { "os": "android", "cpu": "x64" }, "sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g=="], @@ -5541,23 +5538,23 @@ "gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], - "gitbook-v2/next/@next/env": ["@next/env@15.2.0-canary.45", "", {}, "sha512-6XI4H/P7n3unmrRkMZrRDVX69hadQAo3rEqtBhPKo9p/qRWSLt5I0C+1O30Dc/cS3R689Dm/UCYTM4yRL7kCxA=="], + "gitbook-v2/next/@next/env": ["@next/env@15.2.0-canary.74", "", {}, "sha512-REBWD6TnX8M89GsvyuGJCziV2SJOPOZJjuo2y+uN8qlBmVnJQ9qk4tGTRqTygnQREfiKXjumncsGG0xZaWGZbw=="], - "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0-canary.45", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jLirjoHHYYpTknu3bLrtkpjR9qk4F2CKqbrbdrqLAku7DcWadlTOENu0K4Oh/EqZzyH/8d8PfTcDo5XnvEbj3g=="], + "gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0-canary.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R5pdSU94H9LSHXtDpvUWwKWL9p9xYlizenImZ7V1AAqbhiksBfORZHoHLZg6M0I+Ko6PTH2g6vPEAnKMYsH/xA=="], - "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0-canary.45", "", { "os": "darwin", "cpu": "x64" }, "sha512-nIUTv7sWzLNNhvvDKIS1l71ODlC6Z+HtAi8xSNPuTO+9WQzD6rSPdadSkStFYbU/X4SAzJwD5pJyEuBaVIofhg=="], + "gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0-canary.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-2goUXrPl8iYEuS1JGzO17kmF+OTj7cCWX8nL/s/XpFC/iUJoWdcmWohaKP+DvKnS4SlYDdpSt733WaKGTwzvpw=="], - "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0-canary.45", "", { "os": "linux", "cpu": "arm64" }, "sha512-aen3xitlvCUacSBRZzuodAzsEWCKnizNULeU8t4KgjPcCyJkYfg/XpkIN+WWXO8X1CTqQwhUy9IZwXP33xlPww=="], + "gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0-canary.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-FoulYpMof4m6LZJ5DR1agKQ0p0DyfX9ylo5CTzvFNfLmKkYmQWGaxG1KKv4YYaivhgYJ6GgubhXExJo/pycZQQ=="], - "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0-canary.45", "", { "os": "linux", "cpu": "arm64" }, "sha512-S5lBJw//nGhG83wgSCo+Li91UE4Dh/SOsLrnbRGXIcWmg5KFj5a8pyq/iDa215j2Uk1xp6NWYeBfoiy1k48wrg=="], + "gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0-canary.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-gnmoPYieUhFysvzSVAbp83Z0n6MmPf1FsD300EA0OGsdhGcObRP/bPafporanU0XCHt44IgyA5KHwKIAoM3D3A=="], - "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0-canary.45", "", { "os": "linux", "cpu": "x64" }, "sha512-j+mhe/kk2c6sfcmfwt/eK/wHnt3Z30RK9GsejZm1yStN6zuLwGNy1ytEhPDv8Vt2cB4eF1jDWBK+EU9pvxdQhg=="], + "gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0-canary.74", "", { "os": "linux", "cpu": "x64" }, "sha512-pdkOOtywXSeGvdLr1xYHTSOa4O7zFJ1z79nvCnKCyeO690L5wtc6gxt5Kqjc6/FdDuhxRxCMVE3bEYdLmOGH1w=="], - "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0-canary.45", "", { "os": "linux", "cpu": "x64" }, "sha512-evPdXG4q/njsvZWUB/Yo6sQkEgElkq5yreK5/j2+DYdOmRMLshV3QmdoqhWGRmIpcjKpxXIDKqDN14ncpvx5cA=="], + "gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0-canary.74", "", { "os": "linux", "cpu": "x64" }, "sha512-WjleLpUuSYHiX8/fezvIWDBnNCmLjxypWloWaTunGy13A492CM+FjHbjmSNljDwI7LwT71jTZnc7+vM9uHz9Sg=="], - "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0-canary.45", "", { "os": "win32", "cpu": "arm64" }, "sha512-X3bAGeWb0RT+vQ5508mU0OQfNrW1CUYavdHIQGHHcp/y7oq1fktNUqgXcNhKQmoxCvmPv/hxsSvlPTzIc7/Znw=="], + "gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0-canary.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-E7G6Tj9VB09aog/7SeV3WbFdSrDRDaHeV6gtgHc5OuLpP37FwRVgFzBzL4u2MsOxqAkIw0Iq+SMPmagQLWrAWg=="], - "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0-canary.45", "", { "os": "win32", "cpu": "x64" }, "sha512-QqdTEaFzgNQtJw/CTMCBMK6YZngT+fSuTnmzuY0vkWcIGO6UmzWotxxqwJMHj/S9rBsIpHYmi4UxWyDAj/vfXw=="], + "gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0-canary.74", "", { "os": "win32", "cpu": "x64" }, "sha512-VipdINDvcGUpl6XwyhlwIoa3FBLFoMa1+EMyBDBySIH2Teq6HIy6Rqec5AfqDHbf3EKqRNTLx8tWum20cK6tXQ=="], "gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -5571,6 +5568,8 @@ "mdast-util-from-markdown/micromark/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], + "next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + "raw-body/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], "raw-body/http-errors/inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -5613,10 +5612,6 @@ "webpack/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], - "wrangler/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "wrangler/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@aws-sdk/client-dynamodb/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -5753,6 +5748,18 @@ "@babel/highlight/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20241018.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CRySEzjNRoR8frP5AbtJXd1tgVJa5v7bZon9Dh6nljYlhG+piDv8jvOVEUqF3cXXS+M5aXwr4NlozdMvl5g5mg=="], + + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20241018.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y63yWJNTgETDFkY3Ony71/k/G1HRDFIhEzwbT+OWmg1Qbsqa4TquHPVFkgv+OJhpmD3HV9gTBcn/M2QJ/+pGmg=="], + + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20241018.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a2AbSAXNMMiREvN+PwjHdJ5zOzI4+qf8+rb6H/y4HcVbPZN5C2fanxv5Bx7NUHLiMD/W0FrGug1aU+RPUVZC9Q=="], + + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20241018.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iCJ7bjD/+zhlp3IWnkiry180DwdNvak/sVoS98pIAS41aR3gJVzE5BCz/2yTWFdCoUVZ5yKJrv1HhSKgQRBIEw=="], + + "@cloudflare/next-on-pages/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20241018.1", "", { "os": "win32", "cpu": "x64" }, "sha512-qwDVh/KrwEPY82h6tZ1O4BXBAKeGy30BeTr9wvTUVeY9eX/KT73GuEG+ttwiashRfqjOa0Gcqjsfpd913ITFyg=="], + + "@cloudflare/next-on-pages/miniflare/youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "@mapbox/node-pre-gyp/tar/minizlib/rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], @@ -5787,8 +5794,6 @@ "@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="], - "@vercel/hydrogen/ts-morph/@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "@vercel/node/ts-morph/@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "@vercel/redwood/ts-morph/@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], @@ -5799,6 +5804,8 @@ "@vercel/static-config/ts-morph/@ts-morph/common/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "gitbook-v2/next/postcss/nanoid": ["nanoid@3.3.7", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="], + "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/packages/cache-do/package.json b/packages/cache-do/package.json index 8e0453503..df28788b2 100644 --- a/packages/cache-do/package.json +++ b/packages/cache-do/package.json @@ -21,7 +21,7 @@ }, "devDependencies": { "typescript": "^5.5.3", - "wrangler": "3.82.0" + "wrangler": "^3.109.2" }, "scripts": { "generate": "wrangler types --experimental-include-runtime", diff --git a/packages/gitbook-v2/.gitignore b/packages/gitbook-v2/.gitignore index 745b5f209..84254823d 100644 --- a/packages/gitbook-v2/.gitignore +++ b/packages/gitbook-v2/.gitignore @@ -6,3 +6,6 @@ # cloudflare .open-next + +# Symbolic links +public diff --git a/packages/gitbook-v2/README.md b/packages/gitbook-v2/README.md index f27f74448..e69de29bb 100644 --- a/packages/gitbook-v2/README.md +++ b/packages/gitbook-v2/README.md @@ -1,104 +0,0 @@ -# GitBook V2 - -This package contains the code for an experiment new version of the GitBook renderer. - -The goals of this new version are: - -- Optimize rendering to leverage Next.js ISR and static rendering -- Leverage Node.js runtime and bigger instances to render bigger content -- Evaluate Vercel and Cloudflare deployment options - -## Deployment - -#### Vercel - -The new version is deployed automatically to Vercel using a Github Action on every push. - -#### Cloudflare with Opennext-js - -The new version is deployed automatically to Cloudflare using a Github Action on every push. - -## Potential issues - -- [ ] We can't use `dynamicIO` because our root layout needs to fetch the params (https://github.com/vercel/next.js/discussions/71927) -- [ ] How to do `nonce` for static resources when we can't use `headers()` (dynamic API) (https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy) - -## Structure - -#### Middleware: - -Goals: -- Route between dynamic and static routes - - If a token is in the cookies => dynamic route - - If a token is in the url => dynamic route - - Else => static route -- High performances: - - It should not rely on data fetching - -#### URLs to support: - -Content URLs: - -- `docs.company.com/page` - - Middleware: rewrite to `/static/url/docs.company.com/page` - - Page component renders the page -- `docs.company2.com/section/page` - - Middleware: rewrite to `/static/url/docs.company2.com/section/page` - - Page component renders the page -- `docs.company3.com/section/sitespace/page` - - Middleware: rewrite to `/static/url/docs.company3.com/section/sitespace/page` - - Page component renders the page - -Content URLs (adaptive content or visitor authentication): - -- `docs.company.com/page` - - Middleware: - - If a token is in the cookies or url => dynamic route - - Else => rewrite to `/dynamic/url/docs.company.com/page` - - Page component renders the page - - Read the token from the url query params or cookies - - -PDF export URLs: - -- `docs.company.com/page/~gitbook/pdf` - - Middleware: rewrite to `/dynamic/pdf/docs.company.com/page` - - Page component renders the page - -Images resizing: - -- `docs.company.com/~gitbook/image` - - Middleware: rewrite to `/dynamic/image/docs.company.com` - - Route handlers process the request - -Preview URLs: - -- `open.gitbook.com/sites/site_abc/page` (or `open.gitbook.com/~site/site_abc/page` to stay compatible?) - - Middleware: no rewrite - - Page component renders the page (dynamic?) - - Read the token from the url query params - - Read customizations override from the url query params - - -Space PDF URLs: - -- `open.gitbook.com/spaces/space_abc/pdf` - - Middleware: no rewrite - - Page component renders the page (dynamic?) - - Read the token from the url query params - - Read customizations override from the url query params - -Development: - -- `localhost:3000/sites/site_abc/page` - -#### Routes: - -- Content - - [ ] `/static/url/[[...url]]/page.tsx` - - [ ] `/dynamic/url/[[...url]]/page.tsx` -- Images resizing - - [ ] `/dynamic/image/[hostname]/route.ts` -- PDF export - - [ ] `/dynamic/pdf/[[...url]]/page.tsx` - - [ ] `/spaces/[space]/pdf/page.tsx` diff --git a/packages/gitbook-v2/next.config.mjs b/packages/gitbook-v2/next.config.mjs index 0933e409a..9e8191c20 100644 --- a/packages/gitbook-v2/next.config.mjs +++ b/packages/gitbook-v2/next.config.mjs @@ -6,9 +6,27 @@ const nextConfig = { experimental: { useCache: true, + }, - // We can't use dynamicIO because it doesn't accept reading params in the root layout - // dynamicIO: true, + env: { + BUILD_VERSION: (process.env.GITHUB_SHA ?? '').slice(0, 7), + GITBOOK_URL: process.env.GITBOOK_URL, + GITBOOK_ASSETS_PREFIX: process.env.GITBOOK_ASSETS_PREFIX, + GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL, + GITBOOK_ICONS_TOKEN: process.env.GITBOOK_ICONS_TOKEN, + NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, + }, + + assetPrefix: process.env.GITBOOK_ASSETS_PREFIX, + poweredByHeader: false, + + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gitbook.io', + }, + ], }, }; diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 46b091feb..7eee2c545 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -6,17 +6,24 @@ "next": "canary", "react": "^19.0.0", "react-dom": "^19.0.0", - "@gitbook/api": "^0.93.0" + "@gitbook/api": "^0.96.0", + "@sindresorhus/fnv1a": "^3.1.0", + "server-only": "^0.0.1" }, "devDependencies": { - "@opennextjs/cloudflare": "^0.4.3" + "gitbook": "*", + "@opennextjs/cloudflare": "^0.5.3", + "tailwindcss": "^3.4.0", + "postcss": "^8" }, "scripts": { - "dev:v2": "next", + "generate": "rm -rf ./public && cp -r ../gitbook/public ./public", + "dev:v2": "env-cmd --silent -f ../../.env.local next", "build": "next build", "build:v2": "next build", "start": "next start", "build:v2:cloudflare": "opennextjs-cloudflare", - "dev:v2:cloudflare": "wrangler dev --port 8771" + "dev:v2:cloudflare": "wrangler dev --port 8771", + "unit": "bun test" } } diff --git a/packages/gitbook-v2/postcss.config.js b/packages/gitbook-v2/postcss.config.js new file mode 100644 index 000000000..67cdf1a55 --- /dev/null +++ b/packages/gitbook-v2/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/gitbook-v2/src/app/dynamic/url/[...url]/layout.tsx b/packages/gitbook-v2/src/app/dynamic/url/[...url]/layout.tsx deleted file mode 100644 index 76df22824..000000000 --- a/packages/gitbook-v2/src/app/dynamic/url/[...url]/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { SiteContentLayout } from '@/components/routes/SiteContentLayout'; -import { createDynamicSiteContext } from '@/lib/context'; - -export default async function RootLayout({ - params, - children, -}: { - params: Promise<{ url: string[] }>; - children: React.ReactNode; -}) { - const { url } = await params; - const context = await createDynamicSiteContext(url); - - return {children}; -} diff --git a/packages/gitbook-v2/src/app/dynamic/url/[...url]/page.tsx b/packages/gitbook-v2/src/app/dynamic/url/[...url]/page.tsx deleted file mode 100644 index afc48a206..000000000 --- a/packages/gitbook-v2/src/app/dynamic/url/[...url]/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { SiteContentPage } from '@/components/routes/SiteContentPage'; -import { createDynamicSiteContext } from '@/lib/context'; - -export default async function Page({ params }: { params: Promise<{ url: string[] }> }) { - const { url } = await params; - const context = await createDynamicSiteContext(url); - - return ; -} diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/loading.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/loading.tsx new file mode 100644 index 000000000..8892cd8bc --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/loading.tsx @@ -0,0 +1,5 @@ +import { SitePageSkeleton } from '@/components/SitePage'; + +export default function Loading() { + return ; +} diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/not-found.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/not-found.tsx new file mode 100644 index 000000000..c715d5dd4 --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/not-found.tsx @@ -0,0 +1,5 @@ +import { SitePageNotFound } from '@/components/SitePage'; + +export default async function NotFound() { + return ; +} diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/page.tsx new file mode 100644 index 000000000..6efd95d8f --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[pagePath]/page.tsx @@ -0,0 +1,38 @@ +import { getDynamicSiteContext, getPagePathFromParams, RouteParams } from '@v2/app/utils'; +import { + generateSitePageMetadata, + generateSitePageViewport, + SitePage, +} from '@/components/SitePage'; +import { Metadata, Viewport } from 'next'; + +type PageProps = { + params: Promise; + searchParams: Promise<{ fallback?: string }>; +}; + +export default async function Page(props: PageProps) { + const params = await props.params; + const context = await getDynamicSiteContext(params); + const pathname = getPagePathFromParams(params); + + return ; +} + +export async function generateViewport(props: PageProps): Promise { + const context = await getDynamicSiteContext(await props.params); + return generateSitePageViewport(context); +} + +export async function generateMetadata(props: PageProps): Promise { + const [params, searchParams] = await Promise.all([props.params, props.searchParams]); + const context = await getDynamicSiteContext(params); + const pathname = getPagePathFromParams(params); + + return generateSitePageMetadata({ + context, + pageParams: { pathname }, + redirectOnFallback: true, + fallback: !!searchParams.fallback, + }); +} diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx new file mode 100644 index 000000000..c4b956693 --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/layout.tsx @@ -0,0 +1,28 @@ +import { CustomizationRootLayout } from '@/components/RootLayout'; +import { SiteLayout } from '@/components/SiteLayout'; +import { getDynamicSiteContext, RouteLayoutParams } from '@v2/app/utils'; +import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; +import { getThemeFromMiddleware } from '@v2/lib/middleware'; + +export default async function SiteDynamicLayout({ + params, + children, +}: { + params: Promise; + children: React.ReactNode; +}) { + const context = await getDynamicSiteContext(await params); + const forcedTheme = await getThemeFromMiddleware(); + + return ( + + + {children} + + + ); +} diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/loading.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/loading.tsx new file mode 100644 index 000000000..8892cd8bc --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/loading.tsx @@ -0,0 +1,5 @@ +import { SitePageSkeleton } from '@/components/SitePage'; + +export default function Loading() { + return ; +} diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx new file mode 100644 index 000000000..c715d5dd4 --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx @@ -0,0 +1,5 @@ +import { SitePageNotFound } from '@/components/SitePage'; + +export default async function NotFound() { + return ; +} diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx new file mode 100644 index 000000000..4d17111b3 --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx @@ -0,0 +1,44 @@ +import { unstable_cacheTag as cacheTag } from 'next/cache'; +import { getSiteCacheTag } from '@v2/lib/cache'; +import { getPagePathFromParams, getStaticSiteContext, RouteParams } from '@v2/app/utils'; +import { + generateSitePageMetadata, + generateSitePageViewport, + SitePage, +} from '@/components/SitePage'; +import { Metadata, Viewport } from 'next'; + +export const dynamic = 'force-static'; + +type PageProps = { + params: Promise; +}; + +export default async function Page(props: PageProps) { + 'use cache'; + + const params = await props.params; + const context = await getStaticSiteContext(params); + const pathname = getPagePathFromParams(params); + + cacheTag(getSiteCacheTag(context.site.id)); + + return ; +} + +export async function generateViewport(props: PageProps): Promise { + const context = await getStaticSiteContext(await props.params); + return generateSitePageViewport(context); +} + +export async function generateMetadata(props: PageProps): Promise { + const params = await props.params; + const context = await getStaticSiteContext(params); + const pathname = getPagePathFromParams(params); + + return generateSitePageMetadata({ + context, + pageParams: { pathname }, + redirectOnFallback: true, + }); +} diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx new file mode 100644 index 000000000..8bced4064 --- /dev/null +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx @@ -0,0 +1,28 @@ +import { unstable_cacheTag as cacheTag } from 'next/cache'; +import { getSiteCacheTag } from '@v2/lib/cache'; +import { getStaticSiteContext, RouteLayoutParams } from '@v2/app/utils'; +import { CustomizationRootLayout } from '@/components/RootLayout'; +import { SiteLayout } from '@/components/SiteLayout'; +import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; + +export default async function SiteStaticLayout({ + params, + children, +}: { + params: Promise; + children: React.ReactNode; +}) { + 'use cache'; + + const context = await getStaticSiteContext(await params); + + cacheTag(getSiteCacheTag(context.site.id)); + + return ( + + + {children} + + + ); +} diff --git a/packages/gitbook-v2/src/app/static/url/[...url]/layout.tsx b/packages/gitbook-v2/src/app/static/url/[...url]/layout.tsx deleted file mode 100644 index 9949ade59..000000000 --- a/packages/gitbook-v2/src/app/static/url/[...url]/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { unstable_cacheTag as cacheTag } from 'next/cache'; -import { SiteContentLayout } from '@/components/routes/SiteContentLayout'; -import { getSiteCacheTag } from '@/lib/cache'; -import { createStaticSiteContext } from '@/lib/context'; - -export default async function RootLayout({ - params, - children, -}: { - params: Promise<{ url: string[] }>; - children: React.ReactNode; -}) { - 'use cache'; - - const { url } = await params; - const context = await createStaticSiteContext(url); - - cacheTag(getSiteCacheTag(context.siteId)); - - return {children}; -} diff --git a/packages/gitbook-v2/src/app/static/url/[...url]/page.tsx b/packages/gitbook-v2/src/app/static/url/[...url]/page.tsx deleted file mode 100644 index e966f4c08..000000000 --- a/packages/gitbook-v2/src/app/static/url/[...url]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { unstable_cacheTag as cacheTag } from 'next/cache'; -import { createStaticSiteContext } from '@/lib/context'; -import { getSiteCacheTag } from '@/lib/cache'; -import { SiteContentPage } from '@/components/routes/SiteContentPage'; - -export default async function Page({ params }: { params: Promise<{ url: string[] }> }) { - 'use cache'; - - const { url } = await params; - const context = await createStaticSiteContext(url); - - cacheTag(getSiteCacheTag(context.siteId)); - - return ; -} diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts new file mode 100644 index 000000000..34a3bf79e --- /dev/null +++ b/packages/gitbook-v2/src/app/utils.ts @@ -0,0 +1,116 @@ +import { fetchSiteContextByURL } from '@v2/lib/context'; +import { createDataFetcher } from '@v2/lib/data'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from '@v2/lib/env'; +import { createNoopImageResizer } from '@v2/lib/images'; +import { createLinker } from '@v2/lib/links'; +import { headers } from 'next/headers'; + +export type RouteParamMode = 'url-host' | 'url'; + +export type RouteLayoutParams = { + mode: string; + + /** URL encoded site URL */ + siteURL: string; +}; + +export type RouteParams = RouteLayoutParams & { + pagePath: string; +}; + +/** + * Get the static context when rendering statically a site. + */ +export function getStaticSiteContext(params: RouteLayoutParams) { + const url = getSiteURLFromParams(params); + + const dataFetcher = createDataFetcher(); + const linker = createLinkerFromParams(params); + const imageResizer = createNoopImageResizer(); + + return fetchSiteContextByURL( + { + dataFetcher, + linker, + imageResizer, + }, + { + url: url.toString(), + visitorAuthToken: null, + redirectOnError: false, + }, + ); +} + +/** + * Get the site context when rendering dynamically. + * The context will depend on the request. + */ +export async function getDynamicSiteContext(params: RouteLayoutParams) { + const url = getSiteURLFromParams(params); + const headersSet = await headers(); + + const dataFetcher = createDataFetcher({ + apiToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN, + apiEndpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL, + }); + + const linker = createLinkerFromParams(params); + const imageResizer = createNoopImageResizer(); + + return fetchSiteContextByURL( + { + dataFetcher, + linker, + imageResizer, + }, + { + url: url.toString(), + visitorAuthToken: headersSet.get('x-gitbook-visitor-token'), + + // TODO: set it only when the token comes from the cookies. + redirectOnError: true, + }, + ); +} + +/** + * Get the decoded page path from the params. + */ +export function getPagePathFromParams(params: RouteParams) { + const decoded = decodeURIComponent(params.pagePath); + return decoded; +} + +function createLinkerFromParams(params: RouteLayoutParams) { + const url = getSiteURLFromParams(params); + const mode = getModeFromParams(params.mode); + + if (mode === 'url-host') { + return createLinker({ + host: url.host, + pathname: '/', + }); + } + + const gitbookURL = new URL(GITBOOK_URL); + return createLinker({ + protocol: gitbookURL.protocol, + host: gitbookURL.host, + pathname: `/url/${url.host}`, + }); +} + +function getSiteURLFromParams(params: RouteLayoutParams) { + const decoded = decodeURIComponent(params.siteURL); + const url = new URL('https://' + decoded); + return url; +} + +function getModeFromParams(mode: string): RouteParamMode { + if (mode === 'url-host') { + return 'url-host'; + } + + return 'url'; +} diff --git a/packages/gitbook-v2/src/components/routes/SiteContentLayout.tsx b/packages/gitbook-v2/src/components/routes/SiteContentLayout.tsx deleted file mode 100644 index 664475e7d..000000000 --- a/packages/gitbook-v2/src/components/routes/SiteContentLayout.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { GitBookSiteContext } from '@/lib/context'; - -/** - * Layout component to render the site content. - */ -export async function SiteContentLayout({ - context, - children, -}: { - context: GitBookSiteContext; - children: React.ReactNode; -}) { - const { api } = context; - const { data: publishedSite } = await api.orgs.getPublishedContentSite( - context.organizationId, - context.siteId, - ); - - return ( - - -

{publishedSite.site.title}

- {children} - - - ); -} diff --git a/packages/gitbook-v2/src/components/routes/SiteContentPage.tsx b/packages/gitbook-v2/src/components/routes/SiteContentPage.tsx deleted file mode 100644 index a1ebb8c29..000000000 --- a/packages/gitbook-v2/src/components/routes/SiteContentPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { GitBookSiteContext } from '@/lib/context'; - -export async function SiteContentPage({ context }: { context: GitBookSiteContext }) { - const { api } = context; - const { data } = await api.orgs.getPublishedContentSite(context.organizationId, context.siteId); - - return ( -
-

Found site {data.site.id}

-
- ); -} diff --git a/packages/gitbook-v2/src/lib/cache.ts b/packages/gitbook-v2/src/lib/cache.ts index 92e61531e..38e1d334c 100644 --- a/packages/gitbook-v2/src/lib/cache.ts +++ b/packages/gitbook-v2/src/lib/cache.ts @@ -1,3 +1,34 @@ +/** + * Get the cache tag for a site. + */ export function getSiteCacheTag(siteId: string) { - return `sites/${siteId}`; + return `site:${siteId}`; +} + +/** + * Get the cache tag for a hostname. + */ +export function getHostnameCacheTag(hostname: string) { + return `url:${hostname}`; +} + +/** + * Get the cache tag for an OpenAPI spec. + */ +export function getOpenAPISpecCacheTag(organizationId: string, slug: string) { + return `organization:${organizationId}:openapi:${slug}`; +} + +/** + * Get the cache tag for a space. + */ +export function getSpaceCacheTag(spaceId: string) { + return `space:${spaceId}`; +} + +/** + * Get the cache tag for a change request. + */ +export function getChangeRequestCacheTag(spaceId: string, changeRequestId: string) { + return `space:${spaceId}:change-request:${changeRequestId}`; } diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index 8b0fc57d5..cdfeca428 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -1,112 +1,289 @@ -import { headers } from 'next/headers'; +import { + ChangeRequest, + RevisionPage, + RevisionPageDocument, + Site, + SiteCustomizationSettings, + SiteIntegrationScript, + SiteSection, + SiteSectionGroup, + SiteSpace, + SiteStructure, + Space, +} from '@gitbook/api'; import { redirect } from 'next/navigation'; -import { GitBookAPI } from '@gitbook/api'; -import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@/lib/env'; +import { assert } from 'ts-essentials'; +import { getSiteStructureSections } from '@/lib/sites'; +import { createDataFetcher, GitBookDataFetcher } from '@v2/lib/data'; +import { GitBookSpaceLinker, appendPrefixToLinker } from './links'; +import { ImageResizer } from './images'; /** - * Generic context about rendering content. + * Generic context when rendering content. */ -export interface GitBookContext { +export type GitBookBaseContext = { /** - * API client to fetch data from GitBook. + * Data fetcher to fetch data from GitBook. */ - api: GitBookAPI; -} + dataFetcher: GitBookDataFetcher; + + /** + * Linker to generate links in the current space. + */ + linker: GitBookSpaceLinker; + + /** + * Image resizer to resize images. + */ + imageResizer: ImageResizer; +}; + +/** + * Any context when rendering content. + */ +export type GitBookAnyContext = GitBookSpaceContext | GitBookSiteContext | GitBookPageContext; + +/** + * Context when rendering a space content. + */ +export type GitBookSpaceContext = GitBookBaseContext & { + organizationId: string; + + space: Space; + changeRequest: ChangeRequest | null; + + /** ID of the current revision. */ + revisionId: string; + + /** Pages of the space. */ + pages: RevisionPage[]; + + /** Share key of the space. */ + shareKey: string | undefined; +}; + +export type SiteSections = { + list: (SiteSectionGroup | SiteSection)[]; + current: SiteSection; +}; /** * Context when rendering a site. */ -export interface GitBookSiteContext extends GitBookContext { - /** - * ID of the organization. - */ - organizationId: string; +export type GitBookSiteContext = GitBookSpaceContext & { + site: Site; - /** - * Site ID. - */ - siteId: string; -} + /** Current site space. */ + siteSpace: SiteSpace; + + /** All site spaces in the current section / or entire site */ + siteSpaces: SiteSpace[]; + + /** Sections of the site. */ + sections: null | SiteSections; + + /** Customizations of the site. */ + customization: SiteCustomizationSettings; + + /** Structure of the site. */ + structure: SiteStructure; + + /** Scripts to load for the site. */ + scripts: SiteIntegrationScript[]; + + /** Visitor token used to fetch the site */ + visitorAuthToken: string | null; +}; /** - * Create a site context, when rendering a static page. + * Context when rendering a page. */ -export async function createStaticSiteContext(url: string[]): Promise { - const context = createStaticContext(); - return fetchSiteContext(url, context); -} - -/** - * Create a site context, when rendering a dynamic page. - */ -export async function createDynamicSiteContext(url: string[]): Promise { - const context = await createDynamicContext(); - return fetchSiteContext(url, context); -} +export type GitBookPageContext = (GitBookSpaceContext | GitBookSiteContext) & { + page: RevisionPageDocument; +}; /** * Fetch the context of a site for a given URL and a base context. */ -async function fetchSiteContext( - urlParts: string[], - baseContext: GitBookContext, +export async function fetchSiteContextByURL( + baseContext: GitBookBaseContext, + input: { + url: string; + visitorAuthToken: string | null; + redirectOnError: boolean; + }, ): Promise { - const { api } = baseContext; - const url = getURLFromParams(urlParts); - const { data } = await api.urls.getPublishedContentByUrl({ - url, + const { dataFetcher } = baseContext; + const data = await dataFetcher.getPublishedContentByUrl({ + url: input.url, + visitorAuthToken: input.visitorAuthToken, + redirectOnError: input.redirectOnError, }); if ('redirect' in data) { redirect(data.redirect); } + const context = await fetchSiteContextByIds( + { + ...baseContext, + dataFetcher: createDataFetcher({ + apiEndpoint: dataFetcher.apiEndpoint, + apiToken: data.apiToken, + }), + }, + { + organization: data.organization, + site: data.site, + siteSection: data.siteSection, + siteSpace: data.siteSpace, + space: data.space, + shareKey: data.shareKey, + changeRequest: data.changeRequest, + revision: data.revision, + visitorAuthToken: input.visitorAuthToken, + }, + ); + + const siteContext = { + ...context, + linker: appendPrefixToLinker(context.linker, data.basePath), + }; + + return siteContext; +} + +/** + * Fetch a site context by IDs. + */ +export async function fetchSiteContextByIds( + baseContext: GitBookBaseContext, + ids: { + organization: string; + site: string; + siteSection: string | undefined; + siteSpace: string | undefined; + space: string; + shareKey: string | undefined; + changeRequest: string | undefined; + revision: string | undefined; + visitorAuthToken: string | null; + }, +): Promise { + const { dataFetcher } = baseContext; + + const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] = + await Promise.all([ + dataFetcher.getPublishedContentSite({ + organizationId: ids.organization, + siteId: ids.site, + siteShareKey: ids.shareKey, + }), + fetchSpaceContextByIds(baseContext, ids), + ]); + + // override the title with the customization title + // TODO: remove this hack once we have a proper way to handle site customizations + const site = { + ...orgSite, + ...(customizations.site?.title ? { title: customizations.site.title } : {}), + }; + + const sections = ids.siteSection ? parseSiteSectionsList(siteStructure, ids.siteSection) : null; + + const siteSpace = ( + siteStructure.type === 'siteSpaces' && siteStructure.structure + ? siteStructure.structure + : sections?.current.siteSpaces + )?.find((siteSpace) => siteSpace.id === ids.siteSpace); + if (!siteSpace) { + throw new Error('Site space not found'); + } + + const siteSpaces = + siteStructure.type === 'siteSpaces' + ? siteStructure.structure + : (sections?.current.siteSpaces ?? []); + + const customization = (() => { + if (ids.siteSpace) { + const siteSpaceSettings = customizations.siteSpaces[ids.siteSpace]; + if (siteSpaceSettings) { + return siteSpaceSettings; + } + // We got the pointer from an API and customizations from another. + // It's possible that the two are unsynced leading to not found customizations for the space. + // It's better to fallback on customization of the site that displaying an error. + console.warn('Customization not found for site space', ids.siteSpace); + } + return customizations.site; + })(); + + return { + ...spaceContext, + organizationId: ids.organization, + site, + siteSpaces, + siteSpace, + customization, + structure: siteStructure, + sections, + scripts, + visitorAuthToken: ids.visitorAuthToken, + }; +} + +/** + * Fetch a space context by IDs. + */ +export async function fetchSpaceContextByIds( + baseContext: GitBookBaseContext, + ids: { + space: string; + shareKey: string | undefined; + changeRequest: string | undefined; + revision: string | undefined; + }, +): Promise { + const { dataFetcher } = baseContext; + + const [space, changeRequest] = await Promise.all([ + dataFetcher.getSpace({ + spaceId: ids.space, + shareKey: ids.shareKey, + }), + ids.changeRequest + ? dataFetcher.getChangeRequest({ + spaceId: ids.space, + changeRequestId: ids.changeRequest, + }) + : null, + ]); + + const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision; + + const pages = await dataFetcher.getRevisionPages({ + spaceId: ids.space, + revisionId, + // We only care about the Git metadata when the Git sync is enabled, + // otherwise we can optimize performance by not fetching it + metadata: !!space.gitSync, + }); + return { ...baseContext, - api: new GitBookAPI({ - endpoint: api.endpoint, - authToken: data.apiToken, - userAgent: api.userAgent, - }), - siteId: data.site, - organizationId: data.organization, + organizationId: space.organization, + space, + pages, + changeRequest, + revisionId, + shareKey: ids.shareKey, }; } -/** - * Create the base context when rendering statically. - */ -function createStaticContext(): GitBookContext { - const api = new GitBookAPI({ - authToken: GITBOOK_API_TOKEN, - endpoint: GITBOOK_API_URL, - userAgent: GITBOOK_USER_AGENT, - }); - - return { - api, - }; -} - -/** - * Create the base context when rendering dynamically. - * The context will depend on the request. - */ -async function createDynamicContext(): Promise { - const headersSet = await headers(); - - const api = new GitBookAPI({ - authToken: headersSet.get('x-gitbook-token') ?? GITBOOK_API_TOKEN, - endpoint: headersSet.get('x-gitbook-api') ?? GITBOOK_API_URL, - userAgent: GITBOOK_USER_AGENT, - }); - - return { - api, - }; -} - -function getURLFromParams(input: string[]) { - const url = new URL('https://' + input.join('/')); - return url.toString(); +function parseSiteSectionsList(structure: SiteStructure, siteSectionId: string) { + const sections = getSiteStructureSections(structure); + const section = sections.find((section) => section.id === siteSectionId); + assert(section, 'A section must be defined when there are multiple sections'); + return { list: sections, current: section } satisfies SiteSections; } diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts new file mode 100644 index 000000000..fab450f65 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -0,0 +1,465 @@ +import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; +import { ComputedContentSource, GitBookAPI } from '@gitbook/api'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; +import { GitBookDataFetcher } from './types'; +import { + getChangeRequestCacheTag, + getHostnameCacheTag, + getOpenAPISpecCacheTag, + getSiteCacheTag, + getSpaceCacheTag, +} from '../cache'; + +interface DataFetcherInput { + /** + * API host to use. + */ + apiEndpoint: string; + + /** + * API token. + */ + apiToken: string | null; +} + +const commonInput: DataFetcherInput = { + apiEndpoint: GITBOOK_API_URL, + apiToken: GITBOOK_API_TOKEN, +}; + +/** + * Create a data fetcher using an API token. + * The data are being cached by Next.js built-in cache. + */ +export function createDataFetcher(input: DataFetcherInput = commonInput): GitBookDataFetcher { + return { + apiEndpoint: input.apiEndpoint, + + // + // API that are tied to the token + // + getPublishedContentSite(params) { + return getPublishedContentSite(input, { + organizationId: params.organizationId, + siteId: params.siteId, + siteShareKey: params.siteShareKey, + }); + }, + getSiteRedirectBySource(params) { + return getSiteRedirectBySource(input, { + organizationId: params.organizationId, + siteId: params.siteId, + siteShareKey: params.siteShareKey, + source: params.source, + }); + }, + getRevision(params) { + return getRevision(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + metadata: params.metadata, + }); + }, + getRevisionPages(params) { + return getRevisionPages(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + metadata: params.metadata, + }); + }, + getRevisionFile(params) { + return getRevisionFile(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + fileId: params.fileId, + }); + }, + getRevisionPageByPath(params) { + return getRevisionPageByPath(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + path: params.path, + }); + }, + getReusableContent(params) { + return getReusableContent(input, { + spaceId: params.spaceId, + revisionId: params.revisionId, + reusableContentId: params.reusableContentId, + }); + }, + getLatestOpenAPISpecVersionContent(params) { + return getLatestOpenAPISpecVersionContent(input, { + organizationId: params.organizationId, + slug: params.slug, + }); + }, + getSpace(params) { + return getSpace(input, { + spaceId: params.spaceId, + shareKey: params.shareKey, + }); + }, + getChangeRequest(params) { + return getChangeRequest(input, { + spaceId: params.spaceId, + changeRequestId: params.changeRequestId, + }); + }, + getDocument(params) { + return getDocument(input, { + spaceId: params.spaceId, + documentId: params.documentId, + }); + }, + getComputedDocument(params) { + return getComputedDocument(input, { + spaceId: params.spaceId, + source: params.source, + }); + }, + + // + // API that are not tied to the token + // where the data is the same for all users + // + getUserById(userId) { + return getUserById(commonInput, userId); + }, + getPublishedContentByUrl(params) { + return getPublishedContentByUrl(commonInput, { + url: params.url, + visitorAuthToken: params.visitorAuthToken, + redirectOnError: params.redirectOnError, + }); + }, + }; +} + +async function getUserById(input: DataFetcherInput, userId: string) { + 'use cache'; + + try { + const res = await getAPI(input).users.getUserById(userId); + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getSpace( + input: DataFetcherInput, + params: { + spaceId: string; + shareKey: string | undefined; + }, +) { + 'use cache'; + + cacheTag(getSpaceCacheTag(params.spaceId)); + + const res = await getAPI(input).spaces.getSpaceById(params.spaceId, { + shareKey: params.shareKey, + }); + return res.data; +} + +async function getChangeRequest( + input: DataFetcherInput, + params: { + spaceId: string; + changeRequestId: string; + }, +) { + 'use cache'; + + try { + const res = await getAPI(input).spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + ); + cacheTag(getChangeRequestCacheTag(params.spaceId, res.data.id)); + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getRevision( + input: DataFetcherInput, + params: { + spaceId: string; + revisionId: string; + metadata: boolean; + }, +) { + 'use cache'; + + const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, { + metadata: params.metadata, + }); + return res.data; +} + +async function getRevisionPages( + input: DataFetcherInput, + params: { + spaceId: string; + revisionId: string; + metadata: boolean; + }, +) { + 'use cache'; + + const res = await getAPI(input).spaces.listPagesInRevisionById( + params.spaceId, + params.revisionId, + { + metadata: params.metadata, + }, + ); + return res.data.pages; +} + +async function getRevisionFile( + input: DataFetcherInput, + params: { + spaceId: string; + revisionId: string; + fileId: string; + }, +) { + 'use cache'; + + try { + const res = await getAPI(input).spaces.getFileInRevisionById( + params.spaceId, + params.revisionId, + params.fileId, + ); + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getRevisionPageByPath( + input: DataFetcherInput, + params: { + spaceId: string; + revisionId: string; + path: string; + }, +) { + 'use cache'; + + const encodedPath = encodeURIComponent(params.path); + + try { + const res = await getAPI(input).spaces.getPageInRevisionByPath( + params.spaceId, + params.revisionId, + encodedPath, + ); + + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getDocument( + input: DataFetcherInput, + params: { + spaceId: string; + documentId: string; + }, +) { + 'use cache'; + + const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId); + return res.data; +} + +async function getComputedDocument( + input: DataFetcherInput, + params: { + spaceId: string; + source: ComputedContentSource; + }, +) { + 'use cache'; + + const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, { + source: params.source, + }); + return res.data; +} + +async function getReusableContent( + input: DataFetcherInput, + params: { + spaceId: string; + revisionId: string; + reusableContentId: string; + }, +) { + 'use cache'; + + try { + const res = await getAPI(input).spaces.getReusableContentInRevisionById( + params.spaceId, + params.revisionId, + params.reusableContentId, + ); + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getLatestOpenAPISpecVersionContent( + input: DataFetcherInput, + params: { + organizationId: string; + slug: string; + }, +) { + 'use cache'; + + cacheTag(getOpenAPISpecCacheTag(params.organizationId, params.slug)); + + try { + const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent( + params.organizationId, + params.slug, + ); + return res.data; + } catch (error) { + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +async function getPublishedContentByUrl( + input: DataFetcherInput, + params: { + url: string; + visitorAuthToken: string | null; + redirectOnError: boolean; + }, +) { + 'use cache'; + + const { url, visitorAuthToken, redirectOnError } = params; + + const hostname = new URL(url).hostname; + cacheTag(getHostnameCacheTag(hostname)); + + const res = await getAPI(input).urls.getPublishedContentByUrl({ + url, + visitorAuthToken: visitorAuthToken ?? undefined, + redirectOnError, + }); + + if ('site' in res.data) { + cacheTag(getSiteCacheTag(res.data.site)); + } + + return res.data; +} + +async function getPublishedContentSite( + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + }, +) { + 'use cache'; + cacheTag(getSiteCacheTag(params.siteId)); + const res = await getAPI(input).orgs.getPublishedContentSite( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + }, + ); + return res.data; +} + +async function getSiteRedirectBySource( + input: DataFetcherInput, + params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; + }, +) { + 'use cache'; + + try { + const res = await getAPI(input).orgs.getSiteRedirectBySource( + params.organizationId, + params.siteId, + { + shareKey: params.siteShareKey, + source: params.source, + }, + ); + + return res.data; + } catch (error) { + // 422 is returned when the source is invalid + // we don't want to throw but just return null + if (checkHasErrorCode(error, 422)) { + return null; + } + + if (checkHasErrorCode(error, 404)) { + return null; + } + + throw error; + } +} + +function getAPI(input: DataFetcherInput) { + const { apiEndpoint, apiToken } = input; + const api = new GitBookAPI({ + authToken: apiToken ?? undefined, + endpoint: apiEndpoint, + userAgent: GITBOOK_USER_AGENT, + }); + + return api; +} + +function checkHasErrorCode(error: unknown, code: number) { + return error instanceof Error && 'code' in error && error.code === code; +} diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts new file mode 100644 index 000000000..aca6e3723 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './types'; +export * from './utils'; +export * from './urls'; diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts new file mode 100644 index 000000000..66fc8b157 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -0,0 +1,124 @@ +import type * as api from '@gitbook/api'; + +/** + * Generic fetcher for GitBook data. + * It is used between v1 and v2. + */ +export interface GitBookDataFetcher { + /** + * Endpoint of the API. + */ + apiEndpoint: string; + + /** + * Get a user by its ID. + */ + getUserById(userId: string): Promise; + + /** + * Get a published content by its URL. + */ + getPublishedContentByUrl(params: { + url: string; + visitorAuthToken: string | null; + redirectOnError: boolean; + }): Promise; + + /** + * Get a published content site by its organization ID and site ID. + */ + getPublishedContentSite(params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + }): Promise; + + /** + * Get a space by its ID. + */ + getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise; + + /** + * Get a change request by its space ID and change request ID. + */ + getChangeRequest(params: { + spaceId: string; + changeRequestId: string; + }): Promise; + + /** + * Get the revision by its space ID and revision ID. + */ + getRevision(params: { + spaceId: string; + revisionId: string; + metadata: boolean; + }): Promise; + + /** + * Get the revision pages by its space ID and revision ID. + */ + getRevisionPages(params: { + spaceId: string; + revisionId: string; + metadata: boolean; + }): Promise; + + /** + * Get a revision file by its space ID, revision ID and file ID. + */ + getRevisionFile(params: { + spaceId: string; + revisionId: string; + fileId: string; + }): Promise; + + /** + * Get a revision page by its path. + */ + getRevisionPageByPath(params: { + spaceId: string; + revisionId: string; + path: string; + }): Promise; + + /** + * Get a document by its space ID and document ID. + */ + getDocument(params: { spaceId: string; documentId: string }): Promise; + + /** + * Get a computed document by its space ID and computed source. + */ + getComputedDocument(params: { + spaceId: string; + source: api.ComputedContentSource; + }): Promise; + + /** + * Get a reusable content by its space ID, revision ID and reusable content ID. + */ + getReusableContent(params: { + spaceId: string; + revisionId: string; + reusableContentId: string; + }): Promise; + + /** + * Get the latest OpenAPI spec version content by its organization ID and slug. + */ + getLatestOpenAPISpecVersionContent(params: { + organizationId: string; + slug: string; + }): Promise; + + /** + * Get a site redirect by its source path. + */ + getSiteRedirectBySource(params: { + organizationId: string; + siteId: string; + siteShareKey: string | undefined; + source: string; + }): Promise<{ redirect: api.SiteRedirect | null; target: string } | null>; +} diff --git a/packages/gitbook/src/lib/middleware.test.ts b/packages/gitbook-v2/src/lib/data/urls.test.ts similarity index 99% rename from packages/gitbook/src/lib/middleware.test.ts rename to packages/gitbook-v2/src/lib/data/urls.test.ts index 86f03ca5b..898d26654 100644 --- a/packages/gitbook/src/lib/middleware.test.ts +++ b/packages/gitbook-v2/src/lib/data/urls.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'bun:test'; -import { getURLLookupAlternatives, normalizeURL } from './middleware'; +import { getURLLookupAlternatives, normalizeURL } from './urls'; describe('getURLLookupAlternatives', () => { it('should return all URLs up to the root', () => { diff --git a/packages/gitbook-v2/src/lib/data/urls.ts b/packages/gitbook-v2/src/lib/data/urls.ts new file mode 100644 index 000000000..7d931aa91 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/urls.ts @@ -0,0 +1,252 @@ +import { race, tryCatch } from '@/lib/async'; +import { joinPath } from '@/lib/paths'; +import { GitBookAPI, type GitBookAPIError, type PublishedSiteContentLookup } from '@gitbook/api'; +import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; + +/** + * Lookup a content by its URL using the GitBook API. + * To optimize caching, we try multiple lookup alternatives and return the first one that matches. + */ +export async function getPublishedContentByURL(input: { + url: string; + visitorAuthToken: string | null; + redirectOnError: boolean; +}): Promise< + | { data: PublishedSiteContentLookup; error?: undefined } + | { data?: undefined; error: Error | GitBookAPIError } +> { + const lookupURL = new URL(input.url); + const url = stripURLSearch(lookupURL); + const lookup = getURLLookupAlternatives(url); + + console.log( + `lookup content for url "${url.toString()}", with ${lookup.urls.length} alternatives`, + ); + + const result = await race(lookup.urls, async (alternative, { signal }) => { + const api = new GitBookAPI({ + authToken: GITBOOK_API_TOKEN ?? undefined, + endpoint: GITBOOK_API_URL, + userAgent: GITBOOK_USER_AGENT, + }); + + const callResult = await tryCatch( + api.urls.getPublishedContentByUrl( + { + url: alternative.url, + visitorAuthToken: input.visitorAuthToken ?? undefined, + redirectOnError: input.redirectOnError, + cache: true, + }, + { + signal, + headers: { + 'x-gitbook-force-cache': 'true', + }, + }, + ), + ); + + if (callResult.error) { + if (alternative.primary) { + // We only return an error for the primary alternative (full URL), + // as other parts could result in errors due to the URL being incomplete (share links, etc). + return { error: callResult.error }; + } + return null; + } + + const { + data: { data }, + } = callResult; + + if ('redirect' in data) { + if (alternative.primary) { + // Append the path to the redirect URL + // because we might have matched a shorter path and the redirect is relative to it + if (alternative.extraPath) { + if (data.target === 'content') { + const redirect = new URL(data.redirect); + redirect.pathname = joinPath(redirect.pathname, alternative.extraPath); + data.redirect = redirect.toString(); + } else { + const redirect = new URL(data.redirect); + if (redirect.searchParams.has('location')) { + redirect.searchParams.set( + 'location', + joinPath( + redirect.searchParams.get('location') ?? '', + alternative.extraPath, + ), + ); + data.redirect = redirect.toString(); + } + } + } + + return { data }; + } + + return null; + } + + /** + * We use the following criteria to determine if the lookup result is the right one: + * - the primary alternative was resolved (because that's the longest or most inclusive path) + * - the resolution of the site URL is complete (because we want to resolve the deepest path possible) + * + * In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content. + */ + if (alternative.primary || ('site' in data && data.complete)) { + const siteResult: PublishedSiteContentLookup = { + ...data, + changeRequest: data.changeRequest ?? lookup.changeRequest, + revision: data.revision ?? lookup.revision, + basePath: joinPath(data.basePath, lookup.basePath ?? ''), + pathname: joinPath(data.pathname, alternative.extraPath), + }; + return { data: siteResult }; + } + + return null; + }); + + if (!result) { + return { + error: new Error('No content found'), + }; + } + + return result; +} + +/** + * For a given GitBook URL, return a list of alternative URLs that could be matched against to lookup the content. + * The approach is optimized to aim at reusing cached lookup results as much as possible. + * + * The cases can be: + * + * Custom hostname + * - Public content has a custom hostname: docs.company.com/ + * - Public content has a custom hostname with a share-link: docs.company.com// + * - Public content has a custom hostname with a variant: docs.company.com/v// + * - Public content has a custom hostname with a variant and a share-link: docs.company.com//v// + * - Revisions in a custom hostname: docs.company.com/~/revisions// + * - Changes in a custom hostname: docs.company.com/~/changes// + * + * Custom hostname in the organization or GitBook domain (company.gitbook.io) + * - Public content has a custom hostname in the organization: docs.company.com// + * - Public content has a custom hostname in the organization with a share-link: docs.company.com/// + * - Public content has a custom hostname in the organization with a variant: docs.company.com//v// + * - Public content has a custom hostname in the organization with a variant and a share-link: docs.company.com///v// + */ +export function getURLLookupAlternatives(input: URL) { + const url = normalizeURL(input); + + let basePath: string | undefined = undefined; + let changeRequest: string | undefined = undefined; + let revision: string | undefined = undefined; + const alternatives: Array<{ url: string; extraPath: string; primary: boolean }> = []; + + const pushAlternative = (adding: URL, extraPath: string) => { + const existing = alternatives.find((alt) => alt.url === adding.toString()); + if (existing) { + if (existing.extraPath !== extraPath) { + throw new Error( + `Invalid extraPath ${extraPath} for url ${adding.toString()}, already set to ${ + existing.extraPath + }`, + ); + } + return; + } + + alternatives.push({ + url: adding.toString(), + extraPath, + primary: adding.toString() === url.toString(), + }); + }; + + const pathSegments = url.pathname.slice(1).split('/'); + const tildeIndex = pathSegments.indexOf('~'); + + // URL looks like a specific content url (with ~/revisions/ or ~/changes/ in the path) + // We only start matching after the ~/revisions/ or ~/changes/ segment and we ignore everything before it + if ( + tildeIndex >= 0 && + (pathSegments[tildeIndex + 1] === 'revisions' || pathSegments[tildeIndex + 1] === 'changes') + ) { + const tildeIndex = pathSegments.indexOf('~'); + const revisionOrChangeIdIndex = tildeIndex + 2; + + basePath = pathSegments.slice(tildeIndex, revisionOrChangeIdIndex + 1).join('/'); + if (pathSegments[tildeIndex + 1] === 'revisions') { + revision = pathSegments[revisionOrChangeIdIndex]; + } else { + changeRequest = pathSegments[revisionOrChangeIdIndex]; + } + + // Match up to the tilde + const contentURL = new URL(url); + contentURL.pathname = pathSegments.slice(0, tildeIndex).join('/'); + pushAlternative(contentURL, pathSegments.slice(revisionOrChangeIdIndex + 1).join('/')); + } + + // URL looks like a collection url (with /v/ in the path) + // We only start matching after the /v/ segment and we ignore everything before it + // to avoid potentially matching as a page not found under the default space in the collection + else if (pathSegments.includes('v')) { + const collectionURL = new URL(url); + const vIndex = pathSegments.indexOf('v'); + collectionURL.pathname = pathSegments.slice(0, vIndex + 2).join('/'); + + pushAlternative(collectionURL, pathSegments.slice(vIndex + 2).join('/')); + } else { + // Match only with the host, if it can be a custom hostname + // It should cover most cases of custom domains, and with caching, it should be fast. + if (!url.hostname.includes('.gitbook.io') || pathSegments.length === 0) { + const noPathURL = new URL(url); + noPathURL.pathname = '/'; + + pushAlternative(noPathURL, url.pathname.slice(1)); + } + + // Otherwise match with the first four segments of the path + for (let i = 1; i <= 4; i++) { + if (pathSegments.length >= i) { + const shortURL = new URL(url); + shortURL.pathname = pathSegments.slice(0, i).join('/'); + + pushAlternative(shortURL, pathSegments.slice(i).join('/')); + } + } + } + + // Mark the longuest entry to lookup as primary + alternatives.sort((a, b) => b.extraPath.length - a.extraPath.length); + if (alternatives.length > 0) { + alternatives[alternatives.length - 1].primary = true; + } + + return { urls: alternatives, basePath, changeRequest, revision }; +} + +/** + * Normalize a URL to remove duplicate slashes and trailing slashes + * and transform the pathname to lowercase. + */ +export function normalizeURL(url: URL) { + const result = new URL(url); + result.pathname = url.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, ''); + return result; +} + +/** + * Strip the search params from a URL + */ +export function stripURLSearch(url: URL): URL { + const stripped = new URL(url.toString()); + stripped.search = ''; + return stripped; +} diff --git a/packages/gitbook-v2/src/lib/data/utils.ts b/packages/gitbook-v2/src/lib/data/utils.ts new file mode 100644 index 000000000..cbf286a50 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/utils.ts @@ -0,0 +1,19 @@ +import { RevisionPageDocument } from '@gitbook/api'; +import { GitBookDataFetcher } from './types'; + +/** + * Get the document for a page. + */ +export async function getPageDocument( + dataFetcher: GitBookDataFetcher, + spaceId: string, + page: RevisionPageDocument, +) { + if (page.documentId) { + return dataFetcher.getDocument({ spaceId, documentId: page.documentId }); + } else if (page.computed) { + return dataFetcher.getComputedDocument({ spaceId, source: page.computed }); + } + + return null; +} diff --git a/packages/gitbook-v2/src/lib/env.ts b/packages/gitbook-v2/src/lib/env.ts index c2c73587d..9cd682fcf 100644 --- a/packages/gitbook-v2/src/lib/env.ts +++ b/packages/gitbook-v2/src/lib/env.ts @@ -1,3 +1,18 @@ +/** + * Main host on which GitBook is running. + */ +export const GITBOOK_URL = + process.env.NODE_ENV === 'development' + ? 'http://localhost:3000' + : ((process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : undefined) ?? + process.env.GITBOOK_URL ?? + ''); + +/** + * GitBook app URL. + */ +export const GITBOOK_APP_URL = process.env.NEXT_PUBLIC_GITBOOK_APP_URL ?? `https://app.gitbook.com`; + /** * Default GitBook API URL endpoint. */ @@ -13,3 +28,11 @@ export const GITBOOK_API_TOKEN = process.env.GITBOOK_API_TOKEN ?? null; * User agent to use for API requests. */ export const GITBOOK_USER_AGENT = process.env.GITBOOK_USER_AGENT ?? 'GitBook-Open/2.0.0'; + +/** + * Whether to disable tracking of events into site insights. + * This is used to disable tracking in development mode. + */ +export const GITBOOK_DISABLE_TRACKING = Boolean( + !!process.env.GITBOOK_DISABLE_TRACKING || process.env.NODE_ENV !== 'production', +); diff --git a/packages/gitbook/src/lib/images.ts b/packages/gitbook-v2/src/lib/images/createImageResizer.ts similarity index 61% rename from packages/gitbook/src/lib/images.ts rename to packages/gitbook-v2/src/lib/images/createImageResizer.ts index 83406cf33..71afeb939 100644 --- a/packages/gitbook/src/lib/images.ts +++ b/packages/gitbook-v2/src/lib/images/createImageResizer.ts @@ -1,12 +1,10 @@ import 'server-only'; -import { noCacheFetchOptions } from '@/lib/cache/http'; +import type { ImageResizer } from './types'; +import { generateImageSignature, SignatureVersion } from './signatures'; +import { GitBookSpaceLinker } from '../links'; -import { generateImageSignature } from './image-signatures'; -import { getRootUrl } from './links'; -import { getImageAPIUrl } from './urls'; - -export interface CloudflareImageJsonFormat { +interface CloudflareImageJsonFormat { width: number; height: number; original: { @@ -30,6 +28,78 @@ export interface CloudflareImageOptions { quality?: number; } +/** + * Create an image resizer for a rendering context. + */ +export function createImageResizer({ + host, + linker, +}: { + /** The linker to use to create URLs. */ + linker: GitBookSpaceLinker; + /** The host name of the current site. */ + host: string; +}): ImageResizer { + return { + resize: (urlInput) => { + if (!checkIsSizableImageURL(urlInput)) { + return null; + } + + let cachedSignature: { + signature: string; + version: SignatureVersion; + } | null = null; + + return async (options) => { + cachedSignature ??= await generateImageSignature({ + host, + url: urlInput, + }); + + const url = new URL(linker.toAbsoluteURL('/~gitbook/image')); + url.searchParams.set('url', getImageAPIUrl(urlInput)); + + if (options.width) { + url.searchParams.set('width', options.width.toString()); + } + if (options.height) { + url.searchParams.set('height', options.height.toString()); + } + if (options.dpr) { + url.searchParams.set('dpr', options.dpr.toString()); + } + if (options.quality) { + url.searchParams.set('quality', options.quality.toString()); + } + + url.searchParams.set('sign', cachedSignature.signature); + url.searchParams.set('sv', cachedSignature.version); + + return url.toString(); + }; + }, + + getImageSize: async (input, options) => { + if (!checkIsSizableImageURL(input)) { + return null; + } + + return getImageSize(input, options); + }, + }; +} + +/** + * Create an image resizer that doesn't do any resizing. + */ +export function createNoopImageResizer(): ImageResizer { + return { + resize: () => null, + getImageSize: async (input) => null, + }; +} + /** * Return true if images resizing is enabled. */ @@ -74,64 +144,6 @@ export function checkIsSizableImageURL(input: string): boolean { return true; } -interface ResizeImageOptions { - width?: number; - height?: number; - dpr?: number; - quality?: number; -} - -/** - * Create a function to get resized image URLs for a given image URL. - */ -export async function getResizedImageURLFactory( - input: string, -): Promise<((options: ResizeImageOptions) => string) | null> { - if (!checkIsSizableImageURL(input)) { - return null; - } - - const [{ signature, version }, rootUrl] = await Promise.all([ - generateImageSignature(input), - getRootUrl(), - ]); - - return (options) => { - const url = new URL('/~gitbook/image', rootUrl); - url.searchParams.set('url', getImageAPIUrl(input)); - - if (options.width) { - url.searchParams.set('width', options.width.toString()); - } - if (options.height) { - url.searchParams.set('height', options.height.toString()); - } - if (options.dpr) { - url.searchParams.set('dpr', options.dpr.toString()); - } - if (options.quality) { - url.searchParams.set('quality', options.quality.toString()); - } - - url.searchParams.set('sign', signature); - url.searchParams.set('sv', version); - - return url.toString(); - }; -} - -/** - * Create a new URL for an image with resized parameters. - * The URL is signed and verified by the server. - */ -export async function getResizedImageURL( - input: string, - options: ResizeImageOptions, -): Promise { - const factory = await getResizedImageURLFactory(input); - return factory?.(options) ?? input; -} - /** * Get the size of an image. */ @@ -205,7 +217,6 @@ export async function resizeImage( : `image/${resizeOptions.format || 'jpeg'}`, }, signal, - ...noCacheFetchOptions, }, ); @@ -226,3 +237,21 @@ function stringifyOptions(options: CloudflareImageOptions): string { return `${rest}${rest ? ',' : ''}${key}=${value}`; }, ''); } + +/** + * Because of a bug in Cloudflare, 127.0.0.1 is replaced by localhost. + * We protect against it by converting to a special token, and then parsing + * the token in the image API. + */ +const GITBOOK_LOCALHOST_TOKEN = '$GITBOOK_LOCALHOST$'; + +/** + * Prepare a URL for the GitBook Open Image API. + */ +export function getImageAPIUrl(url: string): string { + return url.replaceAll('127.0.0.1', GITBOOK_LOCALHOST_TOKEN); +} + +export function parseImageAPIURL(url: string): string { + return url.replaceAll(GITBOOK_LOCALHOST_TOKEN, '127.0.0.1'); +} diff --git a/packages/gitbook-v2/src/lib/images/index.ts b/packages/gitbook-v2/src/lib/images/index.ts new file mode 100644 index 000000000..1035ac027 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './createImageResizer'; +export * from './signatures'; +export * from './utils'; diff --git a/packages/gitbook/src/lib/image-signatures.ts b/packages/gitbook-v2/src/lib/images/signatures.ts similarity index 76% rename from packages/gitbook/src/lib/image-signatures.ts rename to packages/gitbook-v2/src/lib/images/signatures.ts index 08e042e85..766228e70 100644 --- a/packages/gitbook/src/lib/image-signatures.ts +++ b/packages/gitbook-v2/src/lib/images/signatures.ts @@ -3,8 +3,6 @@ import 'server-only'; import fnv1a from '@sindresorhus/fnv1a'; import type { MaybePromise } from 'p-map'; -import { getHost } from './links'; - /** * GitBook has supported different version of image signing in the past. To maintain backwards * compatibility, we retain the ability to verify older signatures. @@ -16,25 +14,18 @@ export type SignatureVersion = '0' | '1' | '2'; */ export const CURRENT_SIGNATURE_VERSION: SignatureVersion = '2'; -/** - * A mapping of signature versions to signature functions. - */ -const IMAGE_SIGNATURE_FUNCTIONS: Record MaybePromise> = - { - '0': generateSignatureV0, - '1': generateSignatureV1, - '2': generateSignatureV2, - }; +type SignFnInput = { + url: string; + host: string; +}; -export function isSignatureVersion(input: string): input is SignatureVersion { - return Object.keys(IMAGE_SIGNATURE_FUNCTIONS).includes(input); -} +type SignFn = (input: SignFnInput) => MaybePromise; /** * Verify a signature of an image URL */ export async function verifyImageSignature( - input: string, + input: SignFnInput, { signature, version }: { signature: string; version: SignatureVersion }, ): Promise { const generator = IMAGE_SIGNATURE_FUNCTIONS[version]; @@ -48,7 +39,7 @@ export async function verifyImageSignature( * This function is sync. If you need to implement an async version of image signing, you'll need to change * ths signature of this fn and where it's used. */ -export async function generateImageSignature(input: string): Promise<{ +export async function generateImageSignature(input: SignFnInput): Promise<{ signature: string; version: SignatureVersion; }> { @@ -63,17 +54,16 @@ const fnv1aUtf8Buffer = new Uint8Array(512); * Generate a signature for an image. * The signature is relative to the current site being rendered to avoid serving images from other sites on the same domain. */ -async function generateSignatureV2(input: string): Promise { - const hostName = await getHost(); +const generateSignatureV2: SignFn = async (input) => { const all = [ - input, - hostName, // The hostname is used to avoid serving images from other sites on the same domain + input.url, + input.host, // The hostname is used to avoid serving images from other sites on the same domain process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY, ] .filter(Boolean) .join(':'); return fnv1a(all, { utf8Buffer: fnv1aUtf8Buffer }).toString(16); -} +}; // Reused buffer for FNV-1a hashing in the v1 algorithm const fnv1aUtf8BufferV1 = new Uint8Array(512); @@ -83,22 +73,35 @@ const fnv1aUtf8BufferV1 = new Uint8Array(512); * When setting it in a URL, we use version '1' for the 'sv' querystring parameneter * to know that it was the algorithm that was used. */ -function generateSignatureV1(input: string): string { - const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); +const generateSignatureV1: SignFn = async (input) => { + const all = [input.url, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); return fnv1a(all, { utf8Buffer: fnv1aUtf8BufferV1 }).toString(16); -} +}; /** * Initial algorithm used to generate a signature for an image. It didn't use any versioning in the URL. * We still need it to validate older signatures that were generated without versioning * but still exist in previously generated and cached content. */ -async function generateSignatureV0(input: string): Promise { - const all = [input, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); +const generateSignatureV0: SignFn = async (input) => { + const all = [input.url, process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY].filter(Boolean).join(':'); const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(all)); // Convert ArrayBuffer to hex string const hashArray = Array.from(new Uint8Array(hash)); const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); return hashHex; +}; + +/** + * A mapping of signature versions to signature functions. + */ +const IMAGE_SIGNATURE_FUNCTIONS: Record = { + '0': generateSignatureV0, + '1': generateSignatureV1, + '2': generateSignatureV2, +}; + +export function isSignatureVersion(input: string): input is SignatureVersion { + return Object.keys(IMAGE_SIGNATURE_FUNCTIONS).includes(input); } diff --git a/packages/gitbook-v2/src/lib/images/types.ts b/packages/gitbook-v2/src/lib/images/types.ts new file mode 100644 index 000000000..5bc7d6cda --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/types.ts @@ -0,0 +1,29 @@ +export type GetImageSizeOptions = { + dpr?: number; +}; + +export type ResizeImageOptions = GetImageSizeOptions & { + width?: number; + height?: number; + dpr?: number; + quality?: number; +}; + +interface ImageSize { + width: number; + height: number; +} + +export type ImageResizer = { + /** + * Resize an image. + * @param input - The image URL to resize. + * @param options - The options to resize the image. + */ + resize(input: string): null | ((options: ResizeImageOptions) => Promise); + + /** + * Get the size of an image. + */ + getImageSize(input: string, options: GetImageSizeOptions): Promise; +}; diff --git a/packages/gitbook-v2/src/lib/images/utils.ts b/packages/gitbook-v2/src/lib/images/utils.ts new file mode 100644 index 000000000..89506bef9 --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/utils.ts @@ -0,0 +1,17 @@ +import { ImageResizer, ResizeImageOptions } from './types'; + +/** + * Quick utility to get a resized image URL. + */ +export async function getResizedImageURL( + resizer: ImageResizer, + url: string, + options: ResizeImageOptions, +) { + const getURL = resizer.resize(url); + if (!getURL) { + return url; + } + + return await getURL(options); +} diff --git a/packages/gitbook-v2/src/lib/links.ts b/packages/gitbook-v2/src/lib/links.ts new file mode 100644 index 000000000..de297840f --- /dev/null +++ b/packages/gitbook-v2/src/lib/links.ts @@ -0,0 +1,91 @@ +import { RevisionPage, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api'; +import { getPagePath } from '@/lib/pages'; + +/** + * Generic interface to generate links based on a given context. + */ +export interface GitBookSpaceLinker { + /** + * Generate an absolute path for a relative path in the current space content. + */ + toPathInSpace(relativePath: string): string; + + /** + * Generate an absolute path for a page in the current space. + */ + toPathForPage(input: { + pages: RevisionPage[]; + page: RevisionPageDocument | RevisionPageGroup; + anchor?: string; + }): string; + + /** + * Generate an absolute URL for a given path. + */ + toAbsoluteURL(absolutePath: string): string; +} + +/** + * Create a linker to resolve links in a context being served on a specific URL. + */ +export function createLinker( + /** Where the top of the space is served on */ + servedOn: { + protocol?: string; + host: string; + pathname: string; + }, +): GitBookSpaceLinker { + if (servedOn.host.includes('/')) { + throw new Error('Host cannot include a slash'); + } + + const linker: GitBookSpaceLinker = { + toPathInSpace(relativePath: string): string { + return joinPaths(servedOn.pathname, relativePath); + }, + + toAbsoluteURL(absolutePath: string): string { + return `${servedOn.protocol ?? 'https:'}//${joinPaths(servedOn.host, absolutePath)}`; + }, + + toPathForPage({ pages, page, anchor }) { + return linker.toPathInSpace(getPagePath(pages, page)) + (anchor ? '#' + anchor : ''); + }, + }; + + return linker; +} + +/** + * Append a prefix to a linker. + */ +export function appendPrefixToLinker( + linker: GitBookSpaceLinker, + prefix: string, +): GitBookSpaceLinker { + const linkerWithPrefix: GitBookSpaceLinker = { + toPathInSpace(relativePath: string): string { + return linker.toPathInSpace(joinPaths(prefix, relativePath)); + }, + + toAbsoluteURL(absolutePath: string): string { + return linker.toAbsoluteURL(joinPaths(prefix, absolutePath)); + }, + + toPathForPage({ pages, page, anchor }) { + return ( + linkerWithPrefix.toPathInSpace(getPagePath(pages, page)) + + (anchor ? '#' + anchor : '') + ); + }, + }; + + return linkerWithPrefix; +} + +function joinPaths(prefix: string, path: string): string { + const prefixPath = prefix.endsWith('/') ? prefix : prefix + '/'; + const suffixPath = path.startsWith('/') ? path.slice(1) : path; + return prefixPath + suffixPath; +} diff --git a/packages/gitbook-v2/src/lib/middleware.ts b/packages/gitbook-v2/src/lib/middleware.ts new file mode 100644 index 000000000..d5e8f7cef --- /dev/null +++ b/packages/gitbook-v2/src/lib/middleware.ts @@ -0,0 +1,36 @@ +import { CustomizationThemeMode } from '@gitbook/api'; +import { headers } from 'next/headers'; + +export enum MiddlewareHeaders { + URLMode = 'x-gitbook-url-mode', + Theme = 'x-gitbook-theme', + Customization = 'x-gitbook-customization', +} + +/** + * Get the URL mode from the middleware headers. + */ +export async function getURLModeFromMiddleware(): Promise<'url' | 'url-host'> { + const headersList = await headers(); + const mode = headersList.get(MiddlewareHeaders.URLMode); + if (!mode) { + throw new Error('URL mode is not set by the middleware'); + } + + return mode as 'url' | 'url-host'; +} + +/** + * For preview, the theme can be set via query string (?theme=light). + */ +export async function getThemeFromMiddleware() { + const headersList = await headers(); + const queryStringTheme = headersList.get(MiddlewareHeaders.Theme); + if (!queryStringTheme) { + return null; + } + + return queryStringTheme === 'light' + ? CustomizationThemeMode.Light + : CustomizationThemeMode.Dark; +} diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 1dfbbdf4e..be9f555ce 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -1,11 +1,111 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; +import { GitBookAPIError } from '@gitbook/api'; + +import { removeTrailingSlash } from '@/lib/paths'; +import { MiddlewareHeaders } from '@v2/lib/middleware'; +import { getPublishedContentByURL } from '@v2/lib/data'; export const config = { matcher: ['/((?!_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)'], }; -export function middleware(request: NextRequest) { - // No rewrite +type URLWithMode = { url: URL; mode: 'url' | 'url-host' }; + +export async function middleware(request: NextRequest) { + const extracted = extractURL(request); + if (extracted) { + return serveSiteByURL(request, extracted); + } + return NextResponse.next(); } + +/** + * Serve site by URL. + */ +async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) { + const dynamicHeaders = getDynamicHeaders(request); + const { url, mode } = urlWithMode; + + const result = await getPublishedContentByURL({ + url: url.toString(), + visitorAuthToken: null, + redirectOnError: false, + }); + + if (result.error) { + if (result.error instanceof GitBookAPIError) { + return NextResponse.json( + { + error: result.error.message, + }, + { status: result.error.code }, + ); + } else { + throw result.error; + } + } + + const { data } = result; + + if ('redirect' in data) { + return NextResponse.redirect(data.redirect); + } + + const requestHeaders = new Headers(request.headers); + requestHeaders.set(MiddlewareHeaders.URLMode, mode); + if (dynamicHeaders) { + for (const [key, value] of Object.entries(dynamicHeaders)) { + requestHeaders.set(key, value); + } + } + + const route = [ + 'sites', + dynamicHeaders ? 'dynamic' : 'static', + mode, + encodeURIComponent(url.host + data.basePath), + encodeURIComponent(removeTrailingSlash(data.pathname) || '/'), + ].join('/'); + + return NextResponse.rewrite(new URL('/' + route, request.url), { + headers: requestHeaders, + }); +} + +/** + * The URL of the GitBook content can be passed in 2 different ways: + * - Hostname is in the `X-GitBook-Host` header and the pathname is the path in the request URL. + * - The request URL is matching `/url/:url` + */ +function extractURL(request: NextRequest): URLWithMode | null { + const xGitbookHost = request.headers.get('x-gitbook-host'); + if (xGitbookHost) { + return { + url: new URL(`https://${xGitbookHost}${request.nextUrl.pathname}`), + mode: 'url-host', + }; + } + + const prefix = '/url/'; + if (request.nextUrl.pathname.startsWith(prefix)) { + return { + url: new URL(`https://${request.nextUrl.pathname.slice(prefix.length)}`), + mode: 'url', + }; + } + + return null; +} + +/** + * Evaluate if a request is dynamic or static. + */ +function getDynamicHeaders(request: NextRequest): null | Record { + // TODO: + // - check token in query string + // - check token in cookies + // - check special headers or query string + return null; +} diff --git a/packages/gitbook-v2/tailwind.config.ts b/packages/gitbook-v2/tailwind.config.ts new file mode 100644 index 000000000..10e1dd03c --- /dev/null +++ b/packages/gitbook-v2/tailwind.config.ts @@ -0,0 +1,10 @@ +import config from '../gitbook/tailwind.config'; + +export default { + ...config, + content: [ + '../gitbook/src/pages/**/*.{js,ts,jsx,tsx,mdx}', + '../gitbook/src/components/**/*.{js,ts,jsx,tsx,mdx}', + '../gitbook/src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], +}; diff --git a/packages/gitbook-v2/tsconfig.json b/packages/gitbook-v2/tsconfig.json index 3ddc42993..2e01d0153 100644 --- a/packages/gitbook-v2/tsconfig.json +++ b/packages/gitbook-v2/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "noEmit": true, "incremental": true, "module": "esnext", @@ -19,10 +19,26 @@ } ], "paths": { - "@/*": ["./src/*"], - "@v1/*": ["../gitbook/src/*"] - } + "@v2/*": ["./src/*"], + "@/*": ["../gitbook/src/*"] + }, + "types": [ + "bun-types" // add Bun global + ] }, - "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + "../gitbook/types/**/*.d.ts", + "../gitbook/cf-env.d.ts" + ], + "exclude": [ + "node_modules", + "packages/openapi-parser", + "packages/react-openapi", + "packages/react-math", + "packages/gitbook" + ] } diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index f0c503079..ca12c7340 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -6,6 +6,7 @@ import { CustomizationHeaderItem, CustomizationHeaderPreset, CustomizationIconsStyle, + CustomizationLinksStyle, CustomizationLocale, CustomizationSidebarBackgroundStyle, CustomizationSidebarListStyle, @@ -234,6 +235,7 @@ export function getCustomizationURL(partial: DeepPartial -
- - -
- - ); + return ; } diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/not-found.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/not-found.tsx index d218b9aa2..c715d5dd4 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/not-found.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/not-found.tsx @@ -1,39 +1,5 @@ -import { TrackPageViewEvent } from '@/components/Insights'; -import { getSpaceLanguage, t } from '@/intl/server'; -import { getSiteData, getSpaceContentData } from '@/lib/api'; -import { getSiteContentPointer } from '@/lib/pointer'; -import { tcls } from '@/lib/tailwind'; +import { SitePageNotFound } from '@/components/SitePage'; export default async function NotFound() { - const pointer = await getSiteContentPointer(); - const [{ space }, { customization }] = await Promise.all([ - getSpaceContentData(pointer, pointer.siteShareKey), - getSiteData(pointer), - ]); - - const language = getSpaceLanguage(customization); - - return ( -
-
-

- {t(language, 'notfound_title')} -

-

{t(language, 'notfound')}

-
- - {/* Track the page not found as a page view */} - -
- ); + return ; } diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/page.tsx index ca92a870a..70b79df60 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/[[...pathname]]/page.tsx @@ -1,203 +1,56 @@ -import { CustomizationHeaderPreset, CustomizationThemeMode } from '@gitbook/api'; import { Metadata, Viewport } from 'next'; -import { notFound, redirect } from 'next/navigation'; import React from 'react'; -import { PageAside } from '@/components/PageAside'; -import { PageBody, PageCover } from '@/components/PageBody'; -import { PageHrefContext, getAbsoluteHref, getPageHref } from '@/lib/links'; -import { getPagePath, resolveFirstDocument } from '@/lib/pages'; -import { ContentRefContext } from '@/lib/references'; -import { isSpaceIndexable, isPageIndexable } from '@/lib/seo'; -import { getContentTitle } from '@/lib/utils'; - -import { PageClientLayout } from './PageClientLayout'; -import { PagePathParams, fetchPageData, getPathnameParam, normalizePathname } from '../../fetch'; +import { + generateSitePageMetadata, + generateSitePageViewport, + SitePage, + PagePathParams, +} from '@/components/SitePage'; +import { getSiteContentPointer } from '@/lib/pointer'; +import { fetchV1ContextForSitePointer } from '@/lib/v1'; export const runtime = 'edge'; export const dynamic = 'force-dynamic'; +type PageProps = { + params: Promise; + searchParams: Promise<{ fallback?: string }>; +}; + /** * Fetch and render a page. */ -export default async function Page(props: { - params: Promise; - searchParams: Promise<{ fallback?: string }>; -}) { +export default async function Page(props: PageProps) { + const sitePageProps = await getSitePageProps(props); + return ; +} + +export async function generateViewport(): Promise { + const pointer = await getSiteContentPointer(); + const context = await fetchV1ContextForSitePointer(pointer); + + return generateSitePageViewport(context); +} + +export async function generateMetadata(props: PageProps): Promise { + const sitePageProps = await getSitePageProps(props); + return generateSitePageMetadata(sitePageProps); +} + +async function getSitePageProps(props: PageProps) { const { params: rawParams, searchParams: rawSearchParams } = props; const params = await rawParams; const searchParams = await rawSearchParams; - const { - content: contentPointer, - contentTarget, - sections, - space, - site, - customization, - pages, - page, - ancestors, - document, - } = await getPageDataWithFallback({ - pagePathParams: params, - searchParams, + const pointer = await getSiteContentPointer(); + const context = await fetchV1ContextForSitePointer(pointer); + + return { + context, + pageParams: params, redirectOnFallback: true, - }); - - const linksContext: PageHrefContext = {}; - const rawPathname = getPathnameParam(params); - if (!page) { - const pathname = normalizePathname(rawPathname); - if (pathname !== rawPathname) { - // If the pathname was not normalized, redirect to the normalized version - // before trying to resolve the page again - redirect(await getAbsoluteHref(pathname)); - } else { - notFound(); - } - } else if (getPagePath(pages, page) !== rawPathname) { - redirect(await getPageHref(pages, page, linksContext)); - } - - const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; - const withFullPageCover = !!( - page.cover && - page.layout.cover && - page.layout.coverSize === 'full' - ); - const withPageFeedback = customization.feedback.enabled; - - const contentRefContext: ContentRefContext = { - siteContext: contentPointer, - space, - revisionId: contentTarget.revisionId, - pages, - page, - }; - - const withSections = Boolean(sections && sections.list.length > 0); - const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader }; - - return ( - <> - {withFullPageCover && page.cover ? ( - - ) : null} - {/* We use a flex row reverse to render the aside first because the page is streamed. */} -
- - -
- - - - - ); -} - -export async function generateViewport({ - params, -}: { - params: Promise; -}): Promise { - const { customization } = await fetchPageData(await params); - return { - colorScheme: customization.themes.toggeable - ? customization.themes.default === CustomizationThemeMode.Dark - ? 'dark light' - : 'light dark' - : customization.themes.default, - }; -} - -export async function generateMetadata({ - params, - searchParams, -}: { - params: Promise; - searchParams: Promise<{ fallback?: string }>; -}): Promise { - const { space, pages, page, customization, site, ancestors } = await getPageDataWithFallback({ - pagePathParams: await params, - searchParams: await searchParams, - }); - - if (!page) { - notFound(); - } - - return { - title: [page.title, getContentTitle(space, customization, site ?? null)] - .filter(Boolean) - .join(' | '), - description: page.description ?? '', - alternates: { - // Trim trailing slashes in canonical URL to match the redirect behavior - canonical: (await getAbsoluteHref(getPagePath(pages, page), true)).replace(/\/+$/, ''), - }, - openGraph: { - images: [ - customization.socialPreview.url ?? - (await getAbsoluteHref(`~gitbook/ogimage/${page.id}`, true)), - ], - }, - robots: - (await isSpaceIndexable({ space, site: site ?? null })) && - isPageIndexable(ancestors, page) - ? 'index, follow' - : 'noindex, nofollow', - }; -} - -/** - * Fetches the page data matching the requested pathname and fallback to root page when page is not found. - */ -async function getPageDataWithFallback(args: { - pagePathParams: PagePathParams; - searchParams: { fallback?: string }; - redirectOnFallback?: boolean; -}) { - const { pagePathParams, searchParams, redirectOnFallback = false } = args; - - const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams); - - let page = targetPage; - const canFallback = !!searchParams.fallback; - if (!page && canFallback) { - const rootPage = resolveFirstDocument(pages, []); - - if (redirectOnFallback && rootPage?.page) { - redirect(await getPageHref(pages, rootPage?.page)); - } - - page = rootPage?.page; - } - - return { - ...otherPageData, - pages, - page, + fallback: !!searchParams.fallback, }; } diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx index 9c0d17476..e1c89485e 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx @@ -1,25 +1,16 @@ -import { CustomizationThemeMode } from '@gitbook/api'; +import { getThemeFromMiddleware } from '@v2/lib/middleware'; import { Metadata, Viewport } from 'next'; -import { headers } from 'next/headers'; -import { NuqsAdapter } from 'nuqs/adapters/next/app'; import React from 'react'; -import * as ReactDOM from 'react-dom'; -import { AdminToolbar } from '@/components/AdminToolbar'; -import { CookiesToast } from '@/components/Cookies'; -import { LoadIntegrations } from '@/components/Integrations'; -import { SpaceLayout } from '@/components/SpaceLayout'; -import { api } from '@/lib/api'; -import { assetsDomain } from '@/lib/assets'; -import { buildVersion } from '@/lib/build'; +import { + generateSiteLayoutMetadata, + generateSiteLayoutViewport, + SiteLayout, +} from '@/components/SiteLayout'; import { getContentSecurityPolicyNonce } from '@/lib/csp'; -import { getAbsoluteHref, getBaseUrl } from '@/lib/links'; -import { isSpaceIndexable } from '@/lib/seo'; -import { getContentTitle } from '@/lib/utils'; - -import { ClientContexts } from './ClientContexts'; -import { RocketLoaderDetector } from './RocketLoaderDetector'; -import { fetchContentData } from '../fetch'; +import { getSiteContentPointer } from '@/lib/pointer'; +import { shouldTrackEvents } from '@/lib/tracking'; +import { fetchV1ContextForSitePointer } from '@/lib/v1'; export const runtime = 'edge'; export const dynamic = 'force-dynamic'; @@ -31,132 +22,32 @@ export default async function ContentLayout(props: { children: React.ReactNode } const { children } = props; const nonce = await getContentSecurityPolicyNonce(); - const { - content, - space, - contentTarget, - customization, - pages, - site, - spaces, - ancestors, - scripts, - sections, - } = await fetchContentData(); - - const apiCtx = await api(); - ReactDOM.preconnect(apiCtx.client.endpoint); - if (assetsDomain) { - ReactDOM.preconnect(assetsDomain); - } - - scripts.forEach(({ script }) => { - ReactDOM.preload(script, { - as: 'script', - nonce, - }); - }); - - const queryStringTheme = await getQueryStringTheme(); + const context = await fetchLayoutData(); + const queryStringTheme = await getThemeFromMiddleware(); return ( - - - - {children} - - - {scripts.length > 0 ? ( - <> - - {scripts.map(({ script }) => ( -