diff --git a/.github/workflows/deploy-docs-bundle-dev.yml b/.github/workflows/deploy-docs-bundle-dev.yml deleted file mode 100644 index bd9cbbf053..0000000000 --- a/.github/workflows/deploy-docs-bundle-dev.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Deploy @fern-docs/bundle (dev) - -on: - push: - branches: - - main - workflow_dispatch: - -concurrency: - group: app-dev.buildwithfern.com - cancel-in-progress: true - -jobs: - ignore: - runs-on: ubuntu-latest - outputs: - continue: ${{ steps.ignore.outputs.continue }} - steps: - - uses: actions/checkout@v4 - - name: Ignore unchanged files - id: ignore - uses: ./.github/actions/turbo-ignore - with: - token: ${{ secrets.VERCEL_TOKEN }} - project: "app-dev.buildwithfern.com" - package: "@fern-docs/bundle" - environment: "production" - branch: main - - deploy: - needs: ignore - if: needs.ignore.outputs.continue == 1 - runs-on: ubuntu-latest - environment: - name: Production - app-dev.buildwithfern.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - steps: - # set the ref to a specific branch so that the deployment is scoped to that branch (instead of a headless ref) - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name || github.ref }} - - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app-dev.buildwithfern.com --token=${{ secrets.VERCEL_TOKEN }} --environment=production - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/deploy-docs-bundle-preview.yml b/.github/workflows/deploy-docs-bundle-preview.yml deleted file mode 100644 index f0977f045a..0000000000 --- a/.github/workflows/deploy-docs-bundle-preview.yml +++ /dev/null @@ -1,160 +0,0 @@ -name: Preview @fern-docs/bundle - -on: - pull_request: - push: - branches: - - main - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} - cancel-in-progress: true - -jobs: - ignore: - runs-on: ubuntu-latest - outputs: - continue: ${{ steps.ignore.outputs.continue }} - check_changes: ${{ steps.check_changes.outputs.continue }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 2 # used for turbo-ignore - - name: Check for changes in playwright or github actions - id: check_changes - run: | - if git diff --name-only HEAD~1 -- playwright/ .github/workflows/; then - echo "continue=1" >> $GITHUB_OUTPUT - fi - - name: Ignore unchanged files - id: ignore - if: steps.check_changes.outputs.continue != 1 - uses: ./.github/actions/turbo-ignore - with: - token: ${{ secrets.VERCEL_TOKEN }} - project: "app.buildwithfern.com" - package: "@fern-docs/bundle" - environment: "preview" - branch: ${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} - - deploy: - needs: ignore - if: needs.ignore.outputs.continue == 1 || needs.ignore.outputs.check_changes == 1 - runs-on: ubuntu-latest - environment: - name: Preview - app.buildwithfern.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - permissions: write-all # required for the pr-preview comment - steps: - # set the ref to a specific branch so that the deployment is scoped to that branch (instead of a headless ref) - - uses: actions/checkout@v4 - with: - fetch-depth: 2 # used for turbo-ignore - ref: ${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} - - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app.buildwithfern.com --token=${{ secrets.VERCEL_TOKEN }} - if [ -f deployment-url.txt ]; then - pnpm vercel-scripts preview.txt $(cat deployment-url.txt) --token=${{ secrets.VERCEL_TOKEN }} - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT - fi - - - name: Comment PR Preview - uses: thollander/actions-comment-pull-request@v2 - if: github.event_name == 'pull_request' && steps.deploy.outputs.deployment_url - with: - filePath: preview.txt - comment_tag: pr_preview - - analyze: - needs: ignore - if: needs.ignore.outputs.continue == 1 || needs.ignore.outputs.check_changes == 1 - runs-on: ubuntu-latest - permissions: write-all # required for the pr-preview comment - steps: - # set the ref to a specific branch so that the deployment is scoped to that branch (instead of a headless ref) - - uses: actions/checkout@v4 - with: - fetch-depth: 2 # used for turbo-ignore - ref: ${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} - - - uses: ./.github/actions/install - - - name: Build - id: deploy - run: pnpm vercel-scripts deploy app.buildwithfern.com --token=${{ secrets.VERCEL_TOKEN }} --skip-deploy=true - env: - ANALYZE: 1 - - - name: Analyze bundle - run: pnpm --package=nextjs-bundle-analysis dlx report - - - name: Upload analysis - uses: actions/upload-artifact@v4 - with: - name: bundle - path: packages/fern-docs/bundle/.next/analyze/__bundle_analysis.json - - - name: Download base branch bundle stats - uses: dawidd6/action-download-artifact@v6 - if: success() && github.event.number - with: - workflow: deploy-docs-bundle-preview.yml - branch: ${{ github.event.pull_request.base.ref }} - path: packages/fern-docs/bundle/.next/analyze/base - - # https://infrequently.org/2021/03/the-performance-inequality-gap/ - - name: Compare with base branch bundle - if: success() && github.event.number - run: ls -laR packages/fern-docs/bundle/.next/analyze/base && pnpm --package=nextjs-bundle-analysis dlx compare - - - name: Comment PR Bundle Analysis - if: github.event_name == 'pull_request' - uses: thollander/actions-comment-pull-request@v2 - with: - filePath: packages/fern-docs/bundle/.next/analyze/__bundle_analysis_comment.txt - comment_tag: bundle_analysis - - deploy-dev: - needs: ignore - if: needs.ignore.outputs.continue == 1 || needs.ignore.outputs.check_changes == 1 - runs-on: ubuntu-latest - environment: - name: Preview - app-dev.buildwithfern.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - steps: - # set the ref to a specific branch so that the deployment is scoped to that branch (instead of a headless ref) - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref || github.ref_name || github.ref }} - - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app-dev.buildwithfern.com --token=${{ secrets.VERCEL_TOKEN }} - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT - - ete: - needs: - - ignore - - deploy # only runs on fern-prod - if: always() && (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') - uses: ./.github/workflows/playwright.yml - permissions: write-all - with: - deployment_url: ${{ needs.deploy.outputs.deployment_url || '' }} - secrets: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} diff --git a/.github/workflows/deploy-docs-bundle-prod.yml b/.github/workflows/deploy-docs-bundle-prod.yml deleted file mode 100644 index 401de1e962..0000000000 --- a/.github/workflows/deploy-docs-bundle-prod.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: Deploy @fern-docs/bundle - -on: - workflow_dispatch: - push: - tags: - - ui@* - -concurrency: - group: app.buildwithfern.com - cancel-in-progress: true - -jobs: - deploy_app_buildwithfern_com: - runs-on: ubuntu-latest - if: github.ref_type == 'tag' && github.event_name == 'push' - environment: - name: Production - app.buildwithfern.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app.buildwithfern.com --token=${{ secrets.VERCEL_TOKEN }} --environment=production - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT - - deploy_app_ferndocs_com: - runs-on: ubuntu-latest - if: github.ref_type == 'tag' && github.event_name == 'push' - environment: - name: Production - app.ferndocs.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app.ferndocs.com --token=${{ secrets.VERCEL_TOKEN }} --environment=production - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT - - deploy_app-slash_ferndocs_com: - runs-on: ubuntu-latest - if: github.ref_type == 'tag' && github.event_name == 'push' - environment: - name: Production - app-slash.ferndocs.com - url: ${{ steps.deploy.outputs.deployment_url }} - outputs: - deployment_url: ${{ steps.deploy.outputs.deployment_url }} - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/install - - - name: Build & Deploy to Vercel - id: deploy - run: | - pnpm vercel-scripts deploy app-slash.ferndocs.com --token=${{ secrets.VERCEL_TOKEN }} --environment=production - echo "deployment_url=$(cat deployment-url.txt)" >> $GITHUB_OUTPUT - - # note: E2E tests should run before the deployment is promoted - # but currently tests that rely on javascript are not running successfully b/c of assetPrefix issues - # so we we are running the tests after the deployment is promoted for now - # TODO: Fix the tests and run them before promoting the deployment - ete: - needs: - - deploy_app_buildwithfern_com # only the app.buildwithfern.com deployment is an E2E candidate but ideally all deployments should be tested - - promote - if: needs.deploy_app_buildwithfern_com.outputs.deployment_url - uses: ./.github/workflows/playwright.yml - permissions: write-all - with: - deployment_url: ${{ needs.deploy_app_buildwithfern_com.outputs.deployment_url || '' }} - secrets: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - - revalidate-all: - needs: promote - if: success() - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install - - name: Revalidate all app.buildwithfern.com deployments - run: pnpm vercel-scripts revalidate-all app.buildwithfern.com --token ${{ secrets.VERCEL_TOKEN }} - - rollback: - needs: ete - if: failure() - runs-on: ubuntu-latest - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/install - - name: Rollback on failure # remove this step once we switch back to pre-promotion testing - run: | - echo "E2E tests failed. Rolling back deployment" - pnpm vercel-scripts rollback app.buildwithfern.com --token ${{ secrets.VERCEL_TOKEN }} - pnpm vercel-scripts rollback app.ferndocs.com --token ${{ secrets.VERCEL_TOKEN }} - pnpm vercel-scripts rollback app-slash.ferndocs.com --token ${{ secrets.VERCEL_TOKEN }} - - # currently only the custom domains for app.buildwithfern.com deployment should be revalidated - # because the other deployments don't have custom domains (yet) - pnpm vercel-scripts revalidate-all app.buildwithfern.com --token ${{ secrets.VERCEL_TOKEN }} - echo "All docs deployments have been rolled back successfully!" - exit 1 - - promote: - needs: - - deploy_app_buildwithfern_com - - deploy_app_ferndocs_com - - deploy_app-slash_ferndocs_com - # - ete # Ensure that the E2E tests are run successful before promoting - runs-on: ubuntu-latest - strategy: - matrix: - deployment_url: - - ${{ needs.deploy_app_buildwithfern_com.outputs.deployment_url }} - - ${{ needs.deploy_app_ferndocs_com.outputs.deployment_url }} - - ${{ needs.deploy_app-slash_ferndocs_com.outputs.deployment_url }} - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/install - - - name: Promote Deployment - run: pnpm vercel-scripts promote ${{ matrix.deployment_url }} --token ${{ secrets.VERCEL_TOKEN }} - - smoke-test: - needs: promote # Ensure that the deployment is promoted before running smoke tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Smoke Test - env: - NPM_TOKEN: ${{ secrets.FERN_NPM_TOKEN }} - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - run: | - cd smoke-test - npm install -g fern-api - fern generate --docs --instance https://fern-platform-test.docs.buildwithfern.com - VALUE=$(curl https://fern-platform-test.docs.buildwithfern.com/api-reference/imdb/create-movie) - length=${#VALUE} - # Assert that length is over 1000 - if [ $length -gt 1000 ]; then - echo "Length is greater than 1000" - else - exit 1 - fi - - healthchecks: - needs: promote # Ensure that the deployment is promoted before running healthchecks - uses: ./.github/workflows/healthcheck.yml - secrets: inherit diff --git a/.github/workflows/deploy-local-preview-bundle-dryrun.yml b/.github/workflows/deploy-local-preview-bundle-dryrun.yml deleted file mode 100644 index e23f5421e5..0000000000 --- a/.github/workflows/deploy-local-preview-bundle-dryrun.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Deploy @fern-docs/local-preview-bundle (Dry Run) - -on: - pull_request: - paths: - - "packages/cdk/**" - -env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: "buildwithfern" - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - GITHUB_TOKEN: ${{ secrets.FERN_GITHUB_TOKEN }} - -jobs: - dev: - runs-on: ubuntu-latest - container: - image: cimg/node:18.18.2 - steps: - - uses: actions/checkout@v4 - - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-1 - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Install - uses: ./.github/actions/install - - name: Build local preview bundle - run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - - name: Synthesize local preview bundle - run: pnpm --filter=@fern-platform/cdk run synth:dev2 - - prod: - runs-on: ubuntu-latest - container: - image: cimg/node:18.18.2 - steps: - - uses: actions/checkout@v4 - - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-1 - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Install - uses: ./.github/actions/install - - name: Build local preview bundle - run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - - name: Synthesize local preview bundle - run: pnpm --filter=@fern-platform/cdk run synth:prod diff --git a/.github/workflows/deploy-local-preview-bundle.yml b/.github/workflows/deploy-local-preview-bundle.yml deleted file mode 100644 index 25392edd4f..0000000000 --- a/.github/workflows/deploy-local-preview-bundle.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Deploy @fern-docs/local-preview-bundle - -on: - workflow_dispatch: {} - pull_request: {} - push: - branches: [main] - tags: ["*"] - -env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: "buildwithfern" - FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - GITHUB_TOKEN: ${{ secrets.FERN_GITHUB_TOKEN }} - -jobs: - dev: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - container: - image: cimg/node:18.18.2 - steps: - - uses: actions/checkout@v4 - - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-1 - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Install - uses: ./.github/actions/install - - name: Build local preview bundle - run: | - pnpm compile - ENABLE_SOURCE_MAPS=true pnpm turbo --filter=@fern-docs/local-preview-bundle build - - name: Deploy local preview bundle - run: pnpm --filter=@fern-platform/cdk run deploy:dev2 - - prod: - runs-on: ubuntu-latest - if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/ui@') - container: - image: cimg/node:18.18.2 - steps: - - uses: actions/checkout@v4 - - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-region: us-east-1 - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Install - uses: ./.github/actions/install - - name: Build local preview bundle - run: pnpm turbo --filter=@fern-docs/local-preview-bundle build - - name: Deploy local preview bundle - run: pnpm --filter=@fern-platform/cdk run deploy:prod diff --git a/.prettierignore b/.prettierignore index 72e0ee2a60..6a2fa15d50 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,7 +13,6 @@ bundle.c?js ../tests/**/* .circleci .github -.vscode **/fern/** out pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json index 0937b35f57..3317a822f7 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,9 +1,20 @@ { + "tabWidth": 2, "trailingComma": "es5", "plugins": [ "prettier-plugin-packagejson", - "prettier-plugin-organize-imports", + "@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss" ], - "tailwindFunctions": ["clsx", "cn", "cva"] + "tailwindFunctions": ["clsx", "cn", "cva"], + "importOrder": [ + "server-only", + "^(react*|next*)", + "", + "^@fern-*", + "^@/(.*)$", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index 218f4b8234..649efea4d9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,22 +21,17 @@ }, "typescript.enablePromptUseWorkspaceTsdk": true, "editor.formatOnSave": true, - "editor.codeActionsOnSave": [ - "source.fixAll", - "source.sortMembers", - ], + "editor.codeActionsOnSave": ["source.fixAll", "source.sortMembers"], "editor.defaultFormatter": "esbenp.prettier-vscode", - "[ignore]": { - "editor.defaultFormatter": "foxundermoon.shell-format" - }, "scss.validate": false, + "css.validate": false, "typescript.tsdk": "node_modules/typescript/lib", "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "vitest.commandLine": "pnpm test", "vitest.disableWorkspaceWarning": true, - "[shellscript]": { - "editor.defaultFormatter": "foxundermoon.shell-format" + "files.associations": { + "*.css": "tailwindcss", + "*.scss": "tailwindcss" } } diff --git a/clis/generator-cli/package.json b/clis/generator-cli/package.json index 2fabfbb333..b51fd97b19 100644 --- a/clis/generator-cli/package.json +++ b/clis/generator-cli/package.json @@ -34,14 +34,14 @@ "@types/yargs": "^17.0.32", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.15", "depcheck": "^1.4.7", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "eslint": "^9", "execa": "^9.5.1", "prettier": "^3.4.2", "tmp-promise": "^3.0.3", "tsup": "^8.3.5", "typescript": "4.9.5", - "vitest": "^2.1.4", + "vitest": "^3.0.5", "yargs": "^17.4.1" } } diff --git a/clis/generator-cli/src/__test__/testGenerateReadme.ts b/clis/generator-cli/src/__test__/testGenerateReadme.ts index 4748befb5d..4e6cf34faa 100644 --- a/clis/generator-cli/src/__test__/testGenerateReadme.ts +++ b/clis/generator-cli/src/__test__/testGenerateReadme.ts @@ -2,6 +2,7 @@ import { execa } from "execa"; import { writeFile } from "fs/promises"; import path from "path"; import tmp from "tmp-promise"; + import { FernGeneratorCli } from "../configuration/generated"; import * as serializers from "../configuration/generated/serialization"; diff --git a/clis/generator-cli/src/__test__/testGenerateReference.ts b/clis/generator-cli/src/__test__/testGenerateReference.ts index b479811794..8ffcf1141a 100644 --- a/clis/generator-cli/src/__test__/testGenerateReference.ts +++ b/clis/generator-cli/src/__test__/testGenerateReference.ts @@ -2,6 +2,7 @@ import { execa } from "execa"; import { writeFile } from "fs/promises"; import path from "path"; import tmp from "tmp-promise"; + import { FernGeneratorCli } from "../configuration/generated"; import * as serializers from "../configuration/generated/serialization"; diff --git a/clis/generator-cli/src/cli.ts b/clis/generator-cli/src/cli.ts index a63f9f2180..490eb1a8d9 100644 --- a/clis/generator-cli/src/cli.ts +++ b/clis/generator-cli/src/cli.ts @@ -1,14 +1,16 @@ +import fs from "fs"; +import { mkdir, readFile } from "fs/promises"; +import path from "path"; +import { hideBin } from "yargs/helpers"; +import yargs from "yargs/yargs"; + import { AbsoluteFilePath, cwd, doesPathExist, resolve, } from "@fern-api/fs-utils"; -import fs from "fs"; -import { mkdir, readFile } from "fs/promises"; -import path from "path"; -import { hideBin } from "yargs/helpers"; -import yargs from "yargs/yargs"; + import { loadReadmeConfig } from "./configuration/loadReadmeConfig"; import { loadReferenceConfig } from "./configuration/loadReferenceConfig"; import { ReadmeGenerator } from "./readme/ReadmeGenerator"; diff --git a/clis/generator-cli/src/configuration/loadReadmeConfig.ts b/clis/generator-cli/src/configuration/loadReadmeConfig.ts index 60f473f8a4..921a0564d8 100644 --- a/clis/generator-cli/src/configuration/loadReadmeConfig.ts +++ b/clis/generator-cli/src/configuration/loadReadmeConfig.ts @@ -1,5 +1,7 @@ -import { AbsoluteFilePath } from "@fern-api/fs-utils"; import { readFile } from "fs/promises"; + +import { AbsoluteFilePath } from "@fern-api/fs-utils"; + import { FernGeneratorCli } from "./generated"; export async function loadReadmeConfig({ diff --git a/clis/generator-cli/src/configuration/loadReferenceConfig.ts b/clis/generator-cli/src/configuration/loadReferenceConfig.ts index 30d771e436..d2fe3ea9e2 100644 --- a/clis/generator-cli/src/configuration/loadReferenceConfig.ts +++ b/clis/generator-cli/src/configuration/loadReferenceConfig.ts @@ -1,5 +1,7 @@ -import { AbsoluteFilePath } from "@fern-api/fs-utils"; import { readFile } from "fs/promises"; + +import { AbsoluteFilePath } from "@fern-api/fs-utils"; + import { FernGeneratorCli } from "./generated"; export async function loadReferenceConfig({ diff --git a/clis/generator-cli/src/readme/ReadmeGenerator.ts b/clis/generator-cli/src/readme/ReadmeGenerator.ts index a17d3b8c3a..da66508bd2 100644 --- a/clis/generator-cli/src/readme/ReadmeGenerator.ts +++ b/clis/generator-cli/src/readme/ReadmeGenerator.ts @@ -1,6 +1,8 @@ -import { cloneRepository } from "@fern-api/github"; import { camelCase, upperFirst } from "es-toolkit/string"; import fs from "fs"; + +import { cloneRepository } from "@fern-api/github"; + import { FernGeneratorCli } from "../configuration/generated"; import { ReadmeFeature } from "../configuration/generated/api"; import { StreamWriter, StringWriter, Writer } from "../utils/Writer"; diff --git a/clis/generator-cli/src/readme/ReadmeParser.ts b/clis/generator-cli/src/readme/ReadmeParser.ts index c8ee0d909b..c130029b5b 100644 --- a/clis/generator-cli/src/readme/ReadmeParser.ts +++ b/clis/generator-cli/src/readme/ReadmeParser.ts @@ -1,4 +1,5 @@ import { snakeCase } from "es-toolkit/string"; + import { Block } from "./Block"; export interface ParseResult { diff --git a/clis/generator-cli/src/reference/ReferenceGenerator.ts b/clis/generator-cli/src/reference/ReferenceGenerator.ts index 9c168f375a..1ee1dee01a 100644 --- a/clis/generator-cli/src/reference/ReferenceGenerator.ts +++ b/clis/generator-cli/src/reference/ReferenceGenerator.ts @@ -1,4 +1,5 @@ import fs from "fs"; + import { FernGeneratorCli } from "../configuration/generated"; import { EndpointReference, diff --git a/clis/vercel-scripts/src/cli.ts b/clis/vercel-scripts/src/cli.ts index 9b3335a6da..8d6dc8d36a 100644 --- a/clis/vercel-scripts/src/cli.ts +++ b/clis/vercel-scripts/src/cli.ts @@ -1,6 +1,8 @@ -import { VercelClient } from "@fern-fern/vercel"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; + +import { VercelClient } from "@fern-fern/vercel"; + import { deployCommand } from "./commands/deploy.js"; import { getLastDeployCommand } from "./commands/last-deploy.js"; import { promoteCommand } from "./commands/promote.js"; diff --git a/clis/vercel-scripts/src/commands/last-deploy.ts b/clis/vercel-scripts/src/commands/last-deploy.ts index 34d4f9c1bc..90c45e8135 100644 --- a/clis/vercel-scripts/src/commands/last-deploy.ts +++ b/clis/vercel-scripts/src/commands/last-deploy.ts @@ -1,4 +1,5 @@ import { VercelClient } from "@fern-fern/vercel"; + import { writefs } from "../cwd.js"; import { assertValidEnvironment } from "../utils/valid-env.js"; diff --git a/clis/vercel-scripts/src/commands/promote.ts b/clis/vercel-scripts/src/commands/promote.ts index cf61f2e1d8..525805a6f4 100644 --- a/clis/vercel-scripts/src/commands/promote.ts +++ b/clis/vercel-scripts/src/commands/promote.ts @@ -1,4 +1,5 @@ import { VercelClient } from "@fern-fern/vercel"; + import { cleanDeploymentId } from "../utils/clean-id.js"; import { requestPromote } from "../utils/promoter.js"; import { revalidateAllCommand } from "./revalidate-all.js"; diff --git a/clis/vercel-scripts/src/commands/revalidate-all.ts b/clis/vercel-scripts/src/commands/revalidate-all.ts index 217aa2f105..e4a0c7cf97 100644 --- a/clis/vercel-scripts/src/commands/revalidate-all.ts +++ b/clis/vercel-scripts/src/commands/revalidate-all.ts @@ -1,5 +1,6 @@ import { VercelClient } from "@fern-fern/vercel"; import { GetDeploymentResponse } from "@fern-fern/vercel/api/index.js"; + import { cleanDeploymentId } from "../utils/clean-id.js"; import { FernDocsRevalidator } from "../utils/revalidator.js"; diff --git a/clis/vercel-scripts/src/cwd.ts b/clis/vercel-scripts/src/cwd.ts index 4fcc2ad53d..86c56106e2 100644 --- a/clis/vercel-scripts/src/cwd.ts +++ b/clis/vercel-scripts/src/cwd.ts @@ -1,5 +1,6 @@ import { writeFileSync } from "fs"; import { join } from "path"; + import { loggingExeca } from "./utils/loggingExeca.js"; let _cwd: string | undefined; diff --git a/clis/vercel-scripts/src/utils/deployer.ts b/clis/vercel-scripts/src/utils/deployer.ts index e69bb79a8a..6f83fc81a6 100644 --- a/clis/vercel-scripts/src/utils/deployer.ts +++ b/clis/vercel-scripts/src/utils/deployer.ts @@ -1,7 +1,9 @@ -import { Vercel, VercelClient } from "@fern-fern/vercel"; import { readFileSync } from "fs"; import { join } from "path"; import { UnreachableCaseError } from "ts-essentials"; + +import { Vercel, VercelClient } from "@fern-fern/vercel"; + import { cleanDeploymentId } from "./clean-id.js"; import { loggingExeca } from "./loggingExeca.js"; import { requestPromote } from "./promoter.js"; diff --git a/clis/vercel-scripts/src/utils/promoter.ts b/clis/vercel-scripts/src/utils/promoter.ts index 25832fc48b..34380d9ce6 100644 --- a/clis/vercel-scripts/src/utils/promoter.ts +++ b/clis/vercel-scripts/src/utils/promoter.ts @@ -1,4 +1,5 @@ import { Vercel } from "@fern-fern/vercel"; + import { logCommand } from "./loggingExeca.js"; export async function requestPromote( diff --git a/clis/vercel-scripts/src/utils/revalidator.ts b/clis/vercel-scripts/src/utils/revalidator.ts index 96cad276b4..f7ba4803fe 100644 --- a/clis/vercel-scripts/src/utils/revalidator.ts +++ b/clis/vercel-scripts/src/utils/revalidator.ts @@ -1,5 +1,6 @@ import { FernDocsClient } from "@fern-fern/fern-docs-sdk"; import { Vercel, VercelClient } from "@fern-fern/vercel"; + import { logCommand } from "./loggingExeca.js"; const BANNED_DOMAINS = ["vercel.app", "buildwithfern.com", "ferndocs.com"]; diff --git a/eslint.config.mjs b/eslint.config.mjs index 99c111e465..aa0c491a07 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,7 +1,7 @@ // @ts-check - import { FlatCompat } from "@eslint/eslintrc"; import eslint from "@eslint/js"; +import unusedImports from "eslint-plugin-unused-imports"; import vitest from "eslint-plugin-vitest"; import tseslint from "typescript-eslint"; @@ -24,6 +24,22 @@ export default tseslint.config( "**/node_modules", "fern/**", ], + plugins: { + "unused-imports": unusedImports, + }, + rules: { + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { + varsIgnorePattern: "^_", + argsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + }, }, eslint.configs.recommended, @@ -88,16 +104,7 @@ export default tseslint.config( "no-empty": ["error", { allowEmptyCatch: true }], eqeqeq: ["error", "always", { null: "never" }], "@typescript-eslint/no-deprecated": "error", - "@typescript-eslint/no-unused-vars": [ - "error", - { - varsIgnorePattern: "^_", - argsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ], + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-empty-function": [ "error", { @@ -118,6 +125,7 @@ export default tseslint.config( ], "tailwindcss/no-custom-classname": "off", "tailwindcss/classnames-order": "off", + "@typescript-eslint/no-floating-promises": "error", }, }, diff --git a/fern/apis/fdr/definition/api/latest/commons.yml b/fern/apis/fdr/definition/api/latest/commons.yml index c82e81c25a..a5b03de78a 100644 --- a/fern/apis/fdr/definition/api/latest/commons.yml +++ b/fern/apis/fdr/definition/api/latest/commons.yml @@ -14,7 +14,7 @@ types: WithDescription: properties: - description: optional + description: optional WithNamespace: properties: diff --git a/fern/apis/fdr/definition/docs/latest/__package__.yml b/fern/apis/fdr/definition/docs/latest/__package__.yml index 4716727a23..d1a2189d4a 100644 --- a/fern/apis/fdr/definition/docs/latest/__package__.yml +++ b/fern/apis/fdr/definition/docs/latest/__package__.yml @@ -4,28 +4,12 @@ imports: frontmatter: ./frontmatter.yml types: - MdxEngine: - docs: Engine used to render MDX content - enum: - - name: next_mdx_remote - value: next-mdx-remote - docs: https://github.com/hashicorp/next-mdx-remote - - name: mdx_bundler - value: mdx-bundler - docs: https://github.com/kentcdodds/mdx-bundler # note: ResolvedMDX should not be published from the CLI... this should only be set by getStaticProps or getServerSideProps in Next.JS # so that the server can sanitize the code. TODO: mark this as under an internal audience. ResolvedMDX: properties: - engine: MdxEngine code: string frontmatter: frontmatter.Frontmatter scope: map jsxRefs: optional> - - MarkdownText: - discriminated: false - union: - - string - - ResolvedMDX diff --git a/package.json b/package.json index c0171cd480..7ecc64db6d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "repository": "https://github.com/fern-api/fern-platform", "author": "Birch Solutions, Inc.", "scripts": { + "aws:up": "pnpm up -r \"@aws-sdk/*\"", "build": "turbo build", "check-docs-release-blockers": "pnpm tsx packages/scripts/src/cli.ts -- fern-scripts check-docs-release-blockers", "clean": "turbo clean", @@ -32,7 +33,8 @@ "root-package:fix": "pnpm root-package:check --fix", "test": "CI=true turbo test", "test:update": "CI=true turbo test -- -u", - "vercel-scripts": "pnpm --filter=@fern-platform/vercel-scripts vercel-scripts" + "vercel-scripts": "pnpm --filter=@fern-platform/vercel-scripts vercel-scripts", + "vercel:up": "pnpm up -r \"@vercel/*\"" }, "resolutions": { "@babel/core": "7.26.0", @@ -41,25 +43,19 @@ "cookie": "0.7.0", "cross-spawn": "7.0.5", "elliptic": "6.6.0", - "esbuild": "0.24.2", + "esbuild": "0.25.0", "eslint": "9.17.0", - "eslint-config-next": "15.1.2", - "instantsearch.js": "4.75.4", + "eslint-config-next": "15.2.0-canary.64", "jsonpath-plus": "10.0.7", "markdown-to-jsx": "7.4.0", "micromatch": "4.0.8", - "postcss": "8.4.31", - "react": "18.3.1", - "react-dom": "18.3.1", - "tailwindcss": "3.4.17", + "react": "19.0.0", + "react-dom": "19.0.0", "typescript": "5.7.2", "webpack": "5.94.0" }, "dependencies": { - "@radix-ui/colors": "^3.0.0", - "fern-api": "0.41.16", - "get-port": "^7.1.0", - "prettier-plugin-packagejson": "^2.5.6" + "fern-api": "0.41.16" }, "devDependencies": { "@babel/core": "^7.26.0", @@ -70,8 +66,7 @@ "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.17.0", "@playwright/test": "^1.47.1", - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.10", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/express": "^4.17.13", "@types/http-proxy": "^1.17.15", "@types/is-ci": "^3.0.4", @@ -84,17 +79,18 @@ "depcheck": "^1.4.7", "dotenv": "^16.4.5", "eslint": "^9", - "eslint-config-next": "15.1.2", + "eslint-config-next": "15.2.0-canary.64", "eslint-config-prettier": "^9.1.0", "eslint-config-turbo": "^2.3.3", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-tailwindcss": "^3.17.5", + "eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-vitest": "^0.5.4", "execa": "^5.1.1", "express": "^4.21.2", + "get-port": "^7.1.0", "globals": "^15.14.0", "http-proxy-middleware": "^3.0.3", "husky": "^8.0.1", @@ -105,34 +101,51 @@ "lint-staged": "^13.0.3", "playwright": "^1.47.1", "prettier": "^3.4.2", - "prettier-plugin-organize-imports": "^4.1.0", - "prettier-plugin-tailwindcss": "^0.6.9", - "react": "^18", + "prettier-plugin-packagejson": "^2.5.8", + "prettier-plugin-tailwindcss": "^0.6.11", + "react": "19.0.0", "rollup": "^4.22.4", "styled-jsx": "^5.1.2", "stylelint": "^16.1.0", "stylelint-config-recommended": "^14.0.0", "stylelint-config-standard-scss": "^13.0.0", - "stylelint-config-tailwindcss": "^0.0.7", + "stylelint-config-tailwindcss": "^1.0.0", "stylelint-scss": "^6.0.0", - "tailwindcss": "^3", "ts-node": "^10.9.2", "tsx": "^4.19.2", "turbo": "^2.3.3", "typescript": "^5", "typescript-eslint": "^8.18.1", "typescript-plugin-css-modules": "^5.1.0", - "vitest": "^2.1.4" + "vitest": "^3.0.5" }, "dependenciesMeta": { "jsonc-parser@2.2.1": { "unplugged": true } }, - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@9.15.5+sha512.845196026aab1cc3f098a0474b64dfbab2afe7a1b4e91dd86895d8e4aa32a7a6d03049e2d0ad770bbe4de023a7122fb68c1a1d6e0d033c7076085f9d5d4800d4", "engines": { "node": ">=18.18.0", - "pnpm": "^9.4.0" + "pnpm": "^9" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "@prisma/client", + "@prisma/engines", + "@sentry/cli", + "@sentry/profiling-node", + "@swc/core", + "core-js", + "core-js-pure", + "es5-ext", + "esbuild", + "prisma", + "serverless", + "sharp", + "workerd" + ] }, "nextBundleAnalysis": { "buildOutputDirectory": "packages/fern-docs/bundle/.next", diff --git a/packages/cdk/package.json b/packages/cdk/package.json index cff2d0a7ff..8dbf0af348 100644 --- a/packages/cdk/package.json +++ b/packages/cdk/package.json @@ -24,7 +24,7 @@ "dependencies": { "@fern-fern/fern-cloud-sdk": "^0.0.305", "archiver": "^7.0.1", - "aws-cdk-lib": "^2.122.0", + "aws-cdk-lib": "^2.179.0", "constructs": "^10.3.0", "tsx": "^4.7.1" }, @@ -32,7 +32,7 @@ "@fern-platform/configs": "workspace:*", "@types/archiver": "^6.0.2", "@types/node": "^18.7.18", - "aws-cdk": "^2.118.0", + "aws-cdk": "^2.179.0", "prettier": "^3.4.2", "typescript": "^5" } diff --git a/packages/cdk/src/cdk.ts b/packages/cdk/src/cdk.ts index f003cc639d..aa20c1959d 100644 --- a/packages/cdk/src/cdk.ts +++ b/packages/cdk/src/cdk.ts @@ -1,9 +1,11 @@ #!/usr/bin/env node +import * as cdk from "aws-cdk-lib"; + import { - Environments, EnvironmentType, + Environments, } from "@fern-fern/fern-cloud-sdk/api/index"; -import * as cdk from "aws-cdk-lib"; + import { DocsFeStack } from "./docs-fe-stack"; void main(); diff --git a/packages/cdk/src/docs-fe-stack.ts b/packages/cdk/src/docs-fe-stack.ts index 0ee16ea5c1..880daac21c 100644 --- a/packages/cdk/src/docs-fe-stack.ts +++ b/packages/cdk/src/docs-fe-stack.ts @@ -1,4 +1,3 @@ -import { EnvironmentType } from "@fern-fern/fern-cloud-sdk/api"; import archiver from "archiver"; import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; import { AnyPrincipal, PolicyStatement } from "aws-cdk-lib/aws-iam"; @@ -8,6 +7,8 @@ import { Construct } from "constructs"; import * as fs from "fs"; import path from "path"; +import { EnvironmentType } from "@fern-fern/fern-cloud-sdk/api"; + const LOCAL_PREVIEW_BUNDLE_OUT_DIR = path.resolve( __dirname, "../../fern-docs/local-preview-bundle/out" diff --git a/packages/commons/core-utils/package.json b/packages/commons/core-utils/package.json index 836dd9e113..ba08fcc4a9 100644 --- a/packages/commons/core-utils/package.json +++ b/packages/commons/core-utils/package.json @@ -72,6 +72,6 @@ "prettier": "^3.4.2", "stylelint": "^16.1.0", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/commons/core-utils/src/__test__/combineURLs.test.ts b/packages/commons/core-utils/src/__test__/combineURLs.test.ts index f3410218fa..ea37d9d044 100644 --- a/packages/commons/core-utils/src/__test__/combineURLs.test.ts +++ b/packages/commons/core-utils/src/__test__/combineURLs.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; + import { combineURLs } from "../combineURLs"; // Test cases from [Axios](https://github.com/axios/axios/blob/fe7d09bb08fa1c0e414956b7fc760c80459b0a43/test/specs/helpers/combineURLs.spec.js) diff --git a/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts b/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts index 7cfcc8fde3..47a8646b4b 100644 --- a/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts +++ b/packages/commons/core-utils/src/__test__/withDefaultProtocol.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; + import { withDefaultProtocol } from "../withDefaultProtocol"; describe("withDefaultProtocol", () => { diff --git a/packages/commons/core-utils/src/titleCase.ts b/packages/commons/core-utils/src/titleCase.ts index d070917713..0bd919dfee 100644 --- a/packages/commons/core-utils/src/titleCase.ts +++ b/packages/commons/core-utils/src/titleCase.ts @@ -1,4 +1,5 @@ import title from "title"; + import { SPECIAL_TOKENS } from "./specialTokens"; export function titleCase(name: string): string { diff --git a/packages/commons/core-utils/src/withDefaultProtocol.ts b/packages/commons/core-utils/src/withDefaultProtocol.ts index 901fd18ebb..fa2a75b11e 100644 --- a/packages/commons/core-utils/src/withDefaultProtocol.ts +++ b/packages/commons/core-utils/src/withDefaultProtocol.ts @@ -21,6 +21,10 @@ export function withDefaultProtocol( return undefined; } + if (endpoint === "") { + throw new Error(`URL is empty`); + } + // matches any protocol scheme at the beginning of the string (e.g., "http://", "https://", "ftp://") const protocolRegex = /^[a-z]+:\/\//i; diff --git a/packages/commons/fdr-utils/package.json b/packages/commons/fdr-utils/package.json index ceea5c4bda..f1fbb76f53 100644 --- a/packages/commons/fdr-utils/package.json +++ b/packages/commons/fdr-utils/package.json @@ -42,6 +42,6 @@ "prettier": "^3.4.2", "stylelint": "^16.1.0", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/commons/fdr-utils/src/docs.ts b/packages/commons/fdr-utils/src/docs.ts index 52111af5ac..d20957a86a 100644 --- a/packages/commons/fdr-utils/src/docs.ts +++ b/packages/commons/fdr-utils/src/docs.ts @@ -1,6 +1,7 @@ +import { UnreachableCaseError } from "ts-essentials"; + import { DocsV1Read, FdrAPI } from "@fern-api/fdr-sdk/client/types"; import { Availability } from "@fern-api/fdr-sdk/navigation"; -import { UnreachableCaseError } from "ts-essentials"; export function isVersionedNavigationConfig( navigationConfig: DocsV1Read.NavigationConfig diff --git a/packages/commons/github/package.json b/packages/commons/github/package.json index 56e14b2ce0..e1c877140a 100644 --- a/packages/commons/github/package.json +++ b/packages/commons/github/package.json @@ -47,6 +47,6 @@ "simple-git": "^3.24.0", "stylelint": "^16.1.0", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/commons/github/src/ClonedRepository.ts b/packages/commons/github/src/ClonedRepository.ts index 6549db61c5..3c09cc73c8 100644 --- a/packages/commons/github/src/ClonedRepository.ts +++ b/packages/commons/github/src/ClonedRepository.ts @@ -1,5 +1,6 @@ import { lstat, readFile } from "fs/promises"; import path from "path"; + import { README_FILEPATH } from "./constants"; // ClonedRepository is a repository that has been successfully cloned to the local file system diff --git a/packages/commons/github/src/cloneRepository.ts b/packages/commons/github/src/cloneRepository.ts index 2f32cb137a..af645b1046 100644 --- a/packages/commons/github/src/cloneRepository.ts +++ b/packages/commons/github/src/cloneRepository.ts @@ -1,5 +1,6 @@ import simpleGit from "simple-git"; import tmp from "tmp-promise"; + import { ClonedRepository } from "./ClonedRepository"; import { parseRepository } from "./parseRepository"; diff --git a/packages/commons/github/src/deleteBranch.ts b/packages/commons/github/src/deleteBranch.ts index 24148e6024..acbb6529a2 100644 --- a/packages/commons/github/src/deleteBranch.ts +++ b/packages/commons/github/src/deleteBranch.ts @@ -1,4 +1,5 @@ import { SimpleGit } from "simple-git"; + import { DEFAULT_REMOTE_NAME } from "./constants"; export async function deleteBranch( diff --git a/packages/commons/github/src/getLatestTag.ts b/packages/commons/github/src/getLatestTag.ts index 87b25ba4e1..e466d477d3 100644 --- a/packages/commons/github/src/getLatestTag.ts +++ b/packages/commons/github/src/getLatestTag.ts @@ -1,4 +1,5 @@ import { Octokit } from "octokit"; + import { parseRepository } from "./parseRepository"; /** diff --git a/packages/commons/github/src/getOrUpdateBranch.ts b/packages/commons/github/src/getOrUpdateBranch.ts index e68ca367c4..e7733d47bc 100644 --- a/packages/commons/github/src/getOrUpdateBranch.ts +++ b/packages/commons/github/src/getOrUpdateBranch.ts @@ -1,4 +1,5 @@ import { SimpleGit } from "simple-git"; + import { DEFAULT_REMOTE_NAME } from "./constants"; export async function getOrUpdateBranch( diff --git a/packages/commons/loadable/package.json b/packages/commons/loadable/package.json index 9dd792453a..3d853b0ed3 100644 --- a/packages/commons/loadable/package.json +++ b/packages/commons/loadable/package.json @@ -39,6 +39,6 @@ "prettier": "^3.4.2", "stylelint": "^16.1.0", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/commons/loadable/src/utils.ts b/packages/commons/loadable/src/utils.ts index ae9b99099c..49720d804a 100644 --- a/packages/commons/loadable/src/utils.ts +++ b/packages/commons/loadable/src/utils.ts @@ -1,13 +1,14 @@ import { keys } from "@fern-api/ui-core-utils"; + import { + Loadable, + Loading, + NotFailed, failed, isFailed, isLoaded, - Loadable, loaded, - Loading, loading, - NotFailed, } from "./Loadable"; import { visitLoadable } from "./visitor"; diff --git a/packages/commons/loadable/src/visitor.ts b/packages/commons/loadable/src/visitor.ts index 16a83acb8b..74d3efc873 100644 --- a/packages/commons/loadable/src/visitor.ts +++ b/packages/commons/loadable/src/visitor.ts @@ -1,10 +1,11 @@ import { assertNever } from "@fern-api/ui-core-utils"; + import { + Loadable, isFailed, isLoaded, isLoading, isNotStartedLoading, - Loadable, } from "./Loadable"; export function visitLoadable( diff --git a/packages/commons/react-commons/package.json b/packages/commons/react-commons/package.json index 049761a68d..54a53a7df9 100644 --- a/packages/commons/react-commons/package.json +++ b/packages/commons/react-commons/package.json @@ -33,19 +33,20 @@ "es-toolkit": "^1.27.0", "fastdom": "^1.0.12", "immer": "^9.0.15", - "react": "^18", - "ts-essentials": "^10.0.1" + "react": "19.0.0", + "ts-essentials": "^10.0.1", + "zustand": "^5.0.2" }, "devDependencies": { "@fern-platform/configs": "workspace:*", "@types/node": "^18.7.18", - "@types/react": "^18", + "@types/react": "19.0.8", "depcheck": "^1.4.7", "eslint": "^9", "prettier": "^3.4.2", "stylelint": "^16.1.0", "typescript": "^5", - "vite": "^5.4.10", - "vitest": "^2.1.4" + "vite": "^6.1.0", + "vitest": "^3.0.5" } } diff --git a/packages/commons/react-commons/src/factory/createHandlerSetter.ts b/packages/commons/react-commons/src/factory/createHandlerSetter.ts index f537adbe0b..d25280eda9 100644 --- a/packages/commons/react-commons/src/factory/createHandlerSetter.ts +++ b/packages/commons/react-commons/src/factory/createHandlerSetter.ts @@ -1,4 +1,4 @@ -import { useRef, type RefObject } from "react"; +import { type RefObject, useRef } from "react"; type SomeCallback = (...args: TArgs[]) => TResult; export type CallbackSetter = (nextCallback: SomeCallback) => void; diff --git a/packages/commons/react-commons/src/index.ts b/packages/commons/react-commons/src/index.ts index 5a30d2fe6c..a2f33b59c1 100644 --- a/packages/commons/react-commons/src/index.ts +++ b/packages/commons/react-commons/src/index.ts @@ -1,3 +1,5 @@ +"use client"; + export { PREVENT_DEFAULT } from "./preventDefault"; export { STOP_PROPAGATION } from "./stopPropagation"; export { useBooleanState } from "./useBooleanState"; @@ -23,3 +25,6 @@ export { usePrevious } from "./usePrevious"; export { useResizeObserver } from "./useResizeObserver"; export { useTimeout } from "./useTimeout"; export { useWhyDidYouUpdate } from "./useWhyDidYouUpdate"; +export { useLazyRef } from "./useLazyRef"; +export { tunnel } from "./tunnel-rat"; +export { useMinWidth, useIsMobile } from "./useBreakpoint"; diff --git a/packages/fern-docs/search-ui/src/components/tunnel-rat.tsx b/packages/commons/react-commons/src/tunnel-rat.ts similarity index 83% rename from packages/fern-docs/search-ui/src/components/tunnel-rat.tsx rename to packages/commons/react-commons/src/tunnel-rat.ts index 2c20a0b3d2..11b4275784 100644 --- a/packages/fern-docs/search-ui/src/components/tunnel-rat.tsx +++ b/packages/commons/react-commons/src/tunnel-rat.ts @@ -1,6 +1,9 @@ import React, { ReactNode } from "react"; -import { useIsomorphicLayoutEffect } from "swr/_internal"; -import { create, StoreApi } from "zustand"; + +import { StoreApi, create } from "zustand"; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; type Props = { children: React.ReactNode; @@ -14,7 +17,7 @@ type State = { set: StoreApi["setState"]; }; -export default function tunnel(): { +export function tunnel(): { In: (props: Props) => null; Out: () => ReactNode; useHasChildren: () => boolean; @@ -59,10 +62,12 @@ export default function tunnel(): { return null; }, - Out: () => { - const current = useStore((state) => state.only || state.current); - return <>{current}; - }, + Out: () => + React.createElement( + React.Fragment, + null, + useStore((state) => state.only || state.current) + ), useHasChildren: () => useStore((state) => !!state.only || state.current.length > 0), diff --git a/packages/commons/react-commons/src/useBreakpoint.ts b/packages/commons/react-commons/src/useBreakpoint.ts new file mode 100644 index 0000000000..f23759d746 --- /dev/null +++ b/packages/commons/react-commons/src/useBreakpoint.ts @@ -0,0 +1,37 @@ +import React from "react"; + +const MOBILE_BREAKPOINT = 768; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; + +export function useMinWidth(breakpoint: number): boolean { + const [largerThanBreakpoint, setLargerThanBreakpoint] = React.useState< + boolean | undefined + >(() => { + if (typeof window === "undefined") { + return undefined; + } + return window.innerWidth >= breakpoint; + }); + + useIsomorphicLayoutEffect(() => { + requestAnimationFrame(() => { + window.innerWidth >= breakpoint; + }); + + const mql = window.matchMedia(`(min-width: ${breakpoint}px)`); + const onChange = (e: MediaQueryListEvent) => { + setLargerThanBreakpoint(e.matches); + }; + + mql.addEventListener("change", onChange); + return () => mql.removeEventListener("change", onChange); + }, [breakpoint]); + + return !!largerThanBreakpoint; +} + +export function useIsMobile(): boolean { + return !useMinWidth(MOBILE_BREAKPOINT); +} diff --git a/packages/commons/react-commons/src/useCopyToClipboard.ts b/packages/commons/react-commons/src/useCopyToClipboard.ts index 090aa44751..46083424c5 100644 --- a/packages/commons/react-commons/src/useCopyToClipboard.ts +++ b/packages/commons/react-commons/src/useCopyToClipboard.ts @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; + import { useTimeout } from "./useTimeout"; export declare namespace useCopyToClipboard { diff --git a/packages/commons/react-commons/src/useDebouncedCallback.ts b/packages/commons/react-commons/src/useDebouncedCallback.ts index ba5ab2698b..9b06a932ed 100644 --- a/packages/commons/react-commons/src/useDebouncedCallback.ts +++ b/packages/commons/react-commons/src/useDebouncedCallback.ts @@ -1,5 +1,7 @@ +import { type DependencyList, useCallback, useEffect, useRef } from "react"; + import { debounce } from "es-toolkit/function"; -import { useCallback, useEffect, useRef, type DependencyList } from "react"; + import useWillUnmount from "./useWillUnmount"; interface DebounceOptions { diff --git a/packages/commons/react-commons/src/useDeepEquals.ts b/packages/commons/react-commons/src/useDeepEquals.ts index 4663c26d31..bd27bddacf 100644 --- a/packages/commons/react-commons/src/useDeepEquals.ts +++ b/packages/commons/react-commons/src/useDeepEquals.ts @@ -1,6 +1,7 @@ -import { isEqual } from "es-toolkit/predicate"; import React from "react"; +import { isEqual } from "es-toolkit/predicate"; + type UseEffectParams = Parameters; type EffectCallback = UseEffectParams[0]; type DependencyList = UseEffectParams[1]; diff --git a/packages/commons/react-commons/src/useIsDirectlyHovering.ts b/packages/commons/react-commons/src/useIsDirectlyHovering.ts index 8109428f0d..ab3fe770d8 100644 --- a/packages/commons/react-commons/src/useIsDirectlyHovering.ts +++ b/packages/commons/react-commons/src/useIsDirectlyHovering.ts @@ -1,6 +1,7 @@ -import { assertNever } from "@fern-api/ui-core-utils"; import React, { useCallback, useReducer } from "react"; +import { assertNever } from "@fern-api/ui-core-utils"; + export declare namespace useIsDirectlyHovering { export interface Return { isHovering: boolean; diff --git a/packages/commons/react-commons/src/useIsHovering.ts b/packages/commons/react-commons/src/useIsHovering.ts index 94d97594ec..e1fb4886e0 100644 --- a/packages/commons/react-commons/src/useIsHovering.ts +++ b/packages/commons/react-commons/src/useIsHovering.ts @@ -1,13 +1,14 @@ -import { assertNever } from "@fern-api/ui-core-utils"; import { useCallback, useReducer } from "react"; +import { assertNever } from "@fern-api/ui-core-utils"; + export declare namespace useIsHovering { export interface Return { isHovering: boolean; - onMouseOver: () => void; - onMouseLeave: () => void; - onMouseMove: () => void; - onMouseEnter: () => void; + onPointerOver: () => void; + onPointerLeave: () => void; + onPointerMove: () => void; + onPointerEnter: () => void; } } @@ -15,16 +16,16 @@ export function useIsHovering(): useIsHovering.Return { const [state, dispatch] = useReducer( ( previousState: "inside" | "outside" | "hovering", - action: "mouseover" | "mouseleave" | "mousemove" | "mouseenter" + action: "pointerover" | "pointerleave" | "pointermove" | "pointerenter" ) => { switch (action) { - case "mouseover": + case "pointerover": return previousState === "hovering" ? "hovering" : "inside"; - case "mouseleave": + case "pointerleave": return "outside"; - case "mousemove": + case "pointermove": return previousState === "inside" ? "hovering" : previousState; - case "mouseenter": + case "pointerenter": return "hovering"; default: assertNever(action); @@ -35,9 +36,9 @@ export function useIsHovering(): useIsHovering.Return { return { isHovering: state === "hovering", - onMouseOver: useCallback(() => dispatch("mouseover"), []), - onMouseLeave: useCallback(() => dispatch("mouseleave"), []), - onMouseMove: useCallback(() => dispatch("mousemove"), []), - onMouseEnter: useCallback(() => dispatch("mouseenter"), []), + onPointerOver: useCallback(() => dispatch("pointerover"), []), + onPointerLeave: useCallback(() => dispatch("pointerleave"), []), + onPointerMove: useCallback(() => dispatch("pointermove"), []), + onPointerEnter: useCallback(() => dispatch("pointerenter"), []), }; } diff --git a/packages/commons/react-commons/src/useKeyboardCommand.ts b/packages/commons/react-commons/src/useKeyboardCommand.ts index 3a4b0f9f48..faa42908a5 100644 --- a/packages/commons/react-commons/src/useKeyboardCommand.ts +++ b/packages/commons/react-commons/src/useKeyboardCommand.ts @@ -1,9 +1,10 @@ +import { useEffect } from "react"; + import { type Digit, type Platform, type UppercaseLetter, } from "@fern-api/ui-core-utils"; -import { useEffect } from "react"; export declare namespace useKeyboardCommand { export interface Args { diff --git a/packages/commons/react-commons/src/useKeyboardPress.ts b/packages/commons/react-commons/src/useKeyboardPress.ts index e42304b6f9..084e599b43 100644 --- a/packages/commons/react-commons/src/useKeyboardPress.ts +++ b/packages/commons/react-commons/src/useKeyboardPress.ts @@ -1,6 +1,7 @@ -import { type Digit, type UppercaseLetter } from "@fern-api/ui-core-utils"; import { useEffect } from "react"; +import { type Digit, type UppercaseLetter } from "@fern-api/ui-core-utils"; + type Arrow = "Up" | "Down" | "Right" | "Left"; type OtherKey = diff --git a/packages/commons/react-commons/src/useLazyRef.ts b/packages/commons/react-commons/src/useLazyRef.ts new file mode 100644 index 0000000000..8a441f3073 --- /dev/null +++ b/packages/commons/react-commons/src/useLazyRef.ts @@ -0,0 +1,13 @@ +import { type RefObject, useRef } from "react"; + +const EMPTY_VALUE = Symbol("useLazyRef empty value"); + +export const useLazyRef = (init: () => T): RefObject => { + const resultRef = useRef(EMPTY_VALUE); + + if (resultRef.current === EMPTY_VALUE) { + resultRef.current = init(); + } + + return resultRef as RefObject; +}; diff --git a/packages/commons/react-commons/src/useLocalTextState.ts b/packages/commons/react-commons/src/useLocalTextState.ts index df792a811c..6f43988c60 100644 --- a/packages/commons/react-commons/src/useLocalTextState.ts +++ b/packages/commons/react-commons/src/useLocalTextState.ts @@ -1,7 +1,9 @@ -import { assertNever } from "@fern-api/ui-core-utils"; -import produce from "immer"; import { useCallback, useReducer } from "react"; +import produce from "immer"; + +import { assertNever } from "@fern-api/ui-core-utils"; + export interface LocalTextState { value: string; onChange(value: string): void; diff --git a/packages/commons/react-commons/src/usePlatform.ts b/packages/commons/react-commons/src/usePlatform.ts index 8b6698c2a6..ca3fddf347 100644 --- a/packages/commons/react-commons/src/usePlatform.ts +++ b/packages/commons/react-commons/src/usePlatform.ts @@ -1,6 +1,7 @@ -import { getPlatform, type Platform } from "@fern-api/ui-core-utils"; import { useEffect, useState } from "react"; +import { type Platform, getPlatform } from "@fern-api/ui-core-utils"; + // this is updated by the browser let globalPlatform: Platform | undefined; diff --git a/packages/commons/react-commons/src/useResizeObserver.ts b/packages/commons/react-commons/src/useResizeObserver.ts index 7be2398760..4fa462527c 100644 --- a/packages/commons/react-commons/src/useResizeObserver.ts +++ b/packages/commons/react-commons/src/useResizeObserver.ts @@ -1,11 +1,13 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import fastdom from "fastdom"; import { RefObject, useEffect, useMemo, useRef } from "react"; + +import fastdom from "fastdom"; import { noop } from "ts-essentials"; + import { useEventCallback } from "./useEventCallback"; export function useResizeObserver( - ref: RefObject, + ref: RefObject, measure: (entries: ResizeObserverEntry[]) => void ): void { // ResizeObserver is not supported in SSG, so this hook should be disabled on the server-side diff --git a/packages/commons/react-commons/src/useWhyDidYouUpdate.ts b/packages/commons/react-commons/src/useWhyDidYouUpdate.ts index 007ff0ea6b..0016c0f9a3 100644 --- a/packages/commons/react-commons/src/useWhyDidYouUpdate.ts +++ b/packages/commons/react-commons/src/useWhyDidYouUpdate.ts @@ -6,7 +6,7 @@ export function useWhyDidYouUpdate( ): void { // Get a mutable ref object where we can store props ... // ... for comparison next time this hook runs. - const previousProps = useRef>(); + const previousProps = useRef>(null); useEffect(() => { const prev = previousProps.current; diff --git a/packages/commons/react-commons/src/useWillUnmount.ts b/packages/commons/react-commons/src/useWillUnmount.ts index 26e4dcd0fd..6a2bc04a88 100644 --- a/packages/commons/react-commons/src/useWillUnmount.ts +++ b/packages/commons/react-commons/src/useWillUnmount.ts @@ -1,4 +1,5 @@ import { useLayoutEffect, useRef } from "react"; + import createHandlerSetter, { CallbackSetter, } from "./factory/createHandlerSetter"; diff --git a/packages/fdr-sdk/package.json b/packages/fdr-sdk/package.json index d28feeb17d..539cd69590 100644 --- a/packages/fdr-sdk/package.json +++ b/packages/fdr-sdk/package.json @@ -53,14 +53,13 @@ "@ungap/structured-clone": "^1.2.0", "dayjs": "^1.11.11", "es-toolkit": "^1.27.0", + "escape-string-regexp": "^5.0.0", "fast-deep-equal": "^3.1.3", "form-data": "4.0.0", - "formdata-node": "^6.0.3", "js-base64": "3.7.7", "node-fetch": "2.7.0", "qs": "6.12.0", "tinycolor2": "^1.6.0", - "title": "^3.5.3", "ts-essentials": "^10.0.1", "url-join": "5.0.0" }, @@ -72,11 +71,10 @@ "@types/node-fetch": "2.6.9", "@types/qs": "6.9.14", "@types/tinycolor2": "^1.4.6", - "@types/title": "^3.4.3", "@types/ungap__structured-clone": "^1.2.0", "eslint": "^9", "prettier": "^3.4.2", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/fdr-sdk/src/__test__/fixtures.test.ts b/packages/fdr-sdk/src/__test__/fixtures.test.ts index f505c408fd..89e87d7447 100644 --- a/packages/fdr-sdk/src/__test__/fixtures.test.ts +++ b/packages/fdr-sdk/src/__test__/fixtures.test.ts @@ -1,6 +1,8 @@ -import isPlainObject from "@fern-api/ui-core-utils/isPlainObject"; import fs from "fs"; import path from "path"; + +import isPlainObject from "@fern-api/ui-core-utils/isPlainObject"; + import { FernNavigation } from ".."; import * as ApiDefinition from "../api-definition"; import { ApiDefinitionV1ToLatest } from "../api-definition/migrators/v1ToV2"; diff --git a/packages/fdr-sdk/src/__test__/hume.test.ts b/packages/fdr-sdk/src/__test__/hume.test.ts index a215219518..177a9238be 100644 --- a/packages/fdr-sdk/src/__test__/hume.test.ts +++ b/packages/fdr-sdk/src/__test__/hume.test.ts @@ -1,4 +1,5 @@ import { describe } from "vitest"; + import { FernNavigation } from ".."; import { FernNavigationV1ToLatest } from "../navigation/migrators/v1ToV2"; import { readFixture } from "./readFixtures"; diff --git a/packages/fdr-sdk/src/__test__/octoai.test.ts b/packages/fdr-sdk/src/__test__/octoai.test.ts index 1880166235..7a43b9d53f 100644 --- a/packages/fdr-sdk/src/__test__/octoai.test.ts +++ b/packages/fdr-sdk/src/__test__/octoai.test.ts @@ -1,4 +1,5 @@ import { describe } from "vitest"; + import { FernNavigation } from ".."; import { FernNavigationV1ToLatest } from "../navigation/migrators/v1ToV2"; import { readFixture } from "./readFixtures"; diff --git a/packages/fdr-sdk/src/__test__/readFixtures.ts b/packages/fdr-sdk/src/__test__/readFixtures.ts index c0cc6bc1a4..b38156e330 100644 --- a/packages/fdr-sdk/src/__test__/readFixtures.ts +++ b/packages/fdr-sdk/src/__test__/readFixtures.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; + import { DocsV2Read } from "../client"; export function readFixture(fixture: string) { diff --git a/packages/fdr-sdk/src/__test__/testGetAllUrlsFromDocsConfig.ts b/packages/fdr-sdk/src/__test__/testGetAllUrlsFromDocsConfig.ts index 152a751e5c..f91cae46c1 100644 --- a/packages/fdr-sdk/src/__test__/testGetAllUrlsFromDocsConfig.ts +++ b/packages/fdr-sdk/src/__test__/testGetAllUrlsFromDocsConfig.ts @@ -1,4 +1,5 @@ import urljoin from "url-join"; + import { FernNavigation } from ".."; import { NodeCollector } from "../navigation/NodeCollector"; diff --git a/packages/fdr-sdk/src/api-definition/collect-type-tree.ts b/packages/fdr-sdk/src/api-definition/collect-type-tree.ts index bf5a7237ae..696d449522 100644 --- a/packages/fdr-sdk/src/api-definition/collect-type-tree.ts +++ b/packages/fdr-sdk/src/api-definition/collect-type-tree.ts @@ -1,4 +1,3 @@ -import type { MarkdownText } from "../docs"; import type { Availability, TypeId } from "../navigation"; import { coalesceAvailability } from "./availability"; import { LARGE_LOOP_TOLERANCE } from "./const"; @@ -39,7 +38,7 @@ export interface TypeDefinitionTreeItem { * The path to the type definition */ path: KeyPathItem[]; - descriptions: MarkdownText[]; + descriptions: string[]; availability: Availability | undefined; } @@ -67,7 +66,7 @@ export function collectTypeDefinitionTree( const stack: { type: TypeShapeOrReference; path: KeyPathItem[]; - descriptions: MarkdownText[]; + descriptions: string[]; availability: Availability | undefined; visitedTypeIds: Set; }[] = [ diff --git a/packages/fdr-sdk/src/api-definition/endpoint-context.ts b/packages/fdr-sdk/src/api-definition/endpoint-context.ts index 31c4f93510..973df43093 100644 --- a/packages/fdr-sdk/src/api-definition/endpoint-context.ts +++ b/packages/fdr-sdk/src/api-definition/endpoint-context.ts @@ -1,21 +1,36 @@ -import type * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; -import type * as FernNavigation from "@fern-api/fdr-sdk/navigation"; +import type { + EndpointNode, + TypeId, + WebSocketNode, + WebhookNode, +} from "../navigation"; +import type { + ApiDefinition, + AuthScheme, + EndpointDefinition, + ObjectProperty, + TypeDefinition, + WebSocketChannel, + WebhookDefinition, +} from "./latest"; +import { prune } from "./prune"; export type EndpointContext = { - node: FernNavigation.EndpointNode; - endpoint: ApiDefinition.EndpointDefinition; - globalHeaders: ApiDefinition.ObjectProperty[]; - auth: ApiDefinition.AuthScheme | undefined; - types: Record; + node: EndpointNode; + endpoint: EndpointDefinition; + globalHeaders: ObjectProperty[]; + auth: AuthScheme | undefined; + types: Record; }; export function createEndpointContext( - node: FernNavigation.EndpointNode | undefined, - api: ApiDefinition.ApiDefinition | undefined + node: EndpointNode | undefined, + apiDefinition: ApiDefinition | undefined ): EndpointContext | undefined { if (!node) { return undefined; } + const api = apiDefinition != null ? prune(apiDefinition, node) : undefined; const endpoint = api?.endpoints[node.endpointId]; if (!endpoint) { return undefined; @@ -30,20 +45,21 @@ export function createEndpointContext( } export type WebSocketContext = { - node: FernNavigation.WebSocketNode; - channel: ApiDefinition.WebSocketChannel; - globalHeaders: ApiDefinition.ObjectProperty[]; - auth: ApiDefinition.AuthScheme | undefined; - types: Record; + node: WebSocketNode; + channel: WebSocketChannel; + globalHeaders: ObjectProperty[]; + auth: AuthScheme | undefined; + types: Record; }; export function createWebSocketContext( - node: FernNavigation.WebSocketNode | undefined, - api: ApiDefinition.ApiDefinition | undefined + node: WebSocketNode | undefined, + apiDefinition: ApiDefinition | undefined ): WebSocketContext | undefined { if (!node) { return undefined; } + const api = apiDefinition != null ? prune(apiDefinition, node) : undefined; const channel = api?.websockets[node.webSocketId]; if (!channel) { return undefined; @@ -58,18 +74,19 @@ export function createWebSocketContext( } export type WebhookContext = { - node: FernNavigation.WebhookNode; - webhook: ApiDefinition.WebhookDefinition; - types: Record; + node: WebhookNode; + webhook: WebhookDefinition; + types: Record; }; export function createWebhookContext( - node: FernNavigation.WebhookNode | undefined, - api: ApiDefinition.ApiDefinition | undefined + node: WebhookNode | undefined, + apiDefinition: ApiDefinition | undefined ): WebhookContext | undefined { if (!node) { return undefined; } + const api = apiDefinition != null ? prune(apiDefinition, node) : undefined; const webhook = api?.webhooks[node.webhookId]; if (!webhook) { return undefined; diff --git a/packages/fdr-sdk/src/api-definition/migrators/v1ToV2.ts b/packages/fdr-sdk/src/api-definition/migrators/v1ToV2.ts index 883e27bc58..c17e31f575 100644 --- a/packages/fdr-sdk/src/api-definition/migrators/v1ToV2.ts +++ b/packages/fdr-sdk/src/api-definition/migrators/v1ToV2.ts @@ -1,7 +1,9 @@ +import { mapValues } from "es-toolkit/object"; + import { isNonNullish } from "@fern-api/ui-core-utils"; import titleCase from "@fern-api/ui-core-utils/titleCase"; import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; -import { mapValues } from "es-toolkit/object"; + import { APIV1Read } from "../../client"; import { SupportedLanguage } from "../../client/generated/api/resources/api/resources/v1/resources/read/resources/endpoint/types/SupportedLanguage"; import { ROOT_PACKAGE_ID } from "../../navigation/consts"; diff --git a/packages/fdr-sdk/src/api-definition/snippets/SnippetHttpRequest.ts b/packages/fdr-sdk/src/api-definition/snippets/SnippetHttpRequest.ts index c92f32d512..3d68cb93be 100644 --- a/packages/fdr-sdk/src/api-definition/snippets/SnippetHttpRequest.ts +++ b/packages/fdr-sdk/src/api-definition/snippets/SnippetHttpRequest.ts @@ -1,7 +1,9 @@ -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { compact } from "es-toolkit/array"; import { noop } from "ts-essentials"; import urljoin from "url-join"; + +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type * as Latest from "../latest"; interface SnippetHttpRequestBodyJson { diff --git a/packages/fdr-sdk/src/api-definition/snippets/curl.ts b/packages/fdr-sdk/src/api-definition/snippets/curl.ts index 6ce8654eae..b568a4c13b 100644 --- a/packages/fdr-sdk/src/api-definition/snippets/curl.ts +++ b/packages/fdr-sdk/src/api-definition/snippets/curl.ts @@ -1,10 +1,12 @@ +import { compact } from "es-toolkit/array"; +import { UnreachableCaseError } from "ts-essentials"; + import { isNonNullish, isPlainObject, unknownToString, } from "@fern-api/ui-core-utils"; -import { compact } from "es-toolkit/array"; -import { UnreachableCaseError } from "ts-essentials"; + import type * as Latest from "../latest"; import { SnippetHttpRequest, diff --git a/packages/fdr-sdk/src/api-definition/sort-keys.ts b/packages/fdr-sdk/src/api-definition/sort-keys.ts index bcdd905f15..cd5c1df55a 100644 --- a/packages/fdr-sdk/src/api-definition/sort-keys.ts +++ b/packages/fdr-sdk/src/api-definition/sort-keys.ts @@ -1,7 +1,9 @@ -import isPlainObject from "@fern-api/ui-core-utils/isPlainObject"; -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { difference, keyBy } from "es-toolkit/array"; import { mapValues } from "es-toolkit/object"; + +import isPlainObject from "@fern-api/ui-core-utils/isPlainObject"; +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type * as Latest from "./latest"; import { TypeShapeOrReference } from "./types"; import { unwrapDiscriminatedUnionVariant, unwrapObjectType } from "./unwrap"; diff --git a/packages/fdr-sdk/src/api-definition/status-message.ts b/packages/fdr-sdk/src/api-definition/status-message.ts index 6167f6e132..2194432574 100644 --- a/packages/fdr-sdk/src/api-definition/status-message.ts +++ b/packages/fdr-sdk/src/api-definition/status-message.ts @@ -1,6 +1,6 @@ -import type { APIV1Read } from "@fern-api/fdr-sdk/client/types"; +import { HttpMethod } from "./latest"; -type StatusCodeMessagesByMethod = Partial>; +type StatusCodeMessagesByMethod = Partial>; type StatusCodeMessages = Record; @@ -84,7 +84,7 @@ export const STATUS_CODE_MESSAGES_METHOD_OVERRIDES: StatusCodeMessages = { export function getMessageForStatus( statusCode: number, - method?: APIV1Read.HttpMethod + method?: HttpMethod ): string { // return the method-specific message if it exists if (method != null) { diff --git a/packages/fdr-sdk/src/api-definition/transformer.ts b/packages/fdr-sdk/src/api-definition/transformer.ts index daba460ff1..0153044c10 100644 --- a/packages/fdr-sdk/src/api-definition/transformer.ts +++ b/packages/fdr-sdk/src/api-definition/transformer.ts @@ -1,6 +1,6 @@ -import type * as FernDocs from "@fern-api/fdr-sdk/docs"; import identity from "@fern-api/ui-core-utils/identity"; import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import * as Latest from "./latest"; /** @@ -107,10 +107,7 @@ export class Transformer { } public static descriptions( - transformer: ( - description: FernDocs.MarkdownText, - key: string - ) => FernDocs.MarkdownText + transformer: (description: string, key: string) => string ): Transformer { function internalTransformer( withDescription: T, diff --git a/packages/fdr-sdk/src/api-definition/typeid-visitor.ts b/packages/fdr-sdk/src/api-definition/typeid-visitor.ts index 52dfb97a8c..2194b5e7e8 100644 --- a/packages/fdr-sdk/src/api-definition/typeid-visitor.ts +++ b/packages/fdr-sdk/src/api-definition/typeid-visitor.ts @@ -1,5 +1,7 @@ -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { noop } from "ts-essentials"; + +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type * as Latest from "./latest"; export class ApiTypeIdVisitor { diff --git a/packages/fdr-sdk/src/api-definition/unwrap.ts b/packages/fdr-sdk/src/api-definition/unwrap.ts index c8d55df0e8..69869124a8 100644 --- a/packages/fdr-sdk/src/api-definition/unwrap.ts +++ b/packages/fdr-sdk/src/api-definition/unwrap.ts @@ -1,7 +1,8 @@ +import { compact, sortBy } from "es-toolkit/array"; + import { isPlainObject } from "@fern-api/ui-core-utils"; import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; -import { compact, sortBy } from "es-toolkit/array"; -import * as FernDocs from "../docs"; + import { AvailabilityOrder, coalesceAvailability } from "./availability"; import { LOOP_TOLERANCE } from "./const"; import * as Latest from "./latest"; @@ -13,7 +14,7 @@ import type { export type UnwrappedReference = { shape: DereferencedNonOptionalTypeShapeOrReference; availability: Latest.Availability | undefined; - descriptions: FernDocs.MarkdownText[]; + descriptions: string[]; visitedTypeIds: Set; isOptional: boolean; isNullable: boolean; @@ -23,7 +24,7 @@ export type UnwrappedReference = { export type UnwrappedObjectType = { properties: Latest.ObjectProperty[]; extraProperties: Latest.TypeReference | undefined; - descriptions: FernDocs.MarkdownText[]; + descriptions: string[]; visitedTypeIds: Set; }; @@ -77,7 +78,7 @@ export function unwrapReference( let isOptional = false; let isNullable = false; const defaults: InternalDefaultValue[] = []; - const descriptions: FernDocs.MarkdownText[] = []; + const descriptions: string[] = []; const availabilities: Latest.Availability[] = []; const visitedTypeIds = new Set(); @@ -234,7 +235,7 @@ export function unwrapObjectType( const directProperties = object.properties; const extraProperties = object.extraProperties; - const descriptions: FernDocs.MarkdownText[] = []; + const descriptions: string[] = []; const visitedTypeIds = new Set(); const extendedProperties = object.extends.flatMap( (typeId): Latest.ObjectProperty[] => { diff --git a/packages/fdr-sdk/src/api-definition/url.ts b/packages/fdr-sdk/src/api-definition/url.ts index 1a5233c25b..a284272493 100644 --- a/packages/fdr-sdk/src/api-definition/url.ts +++ b/packages/fdr-sdk/src/api-definition/url.ts @@ -1,6 +1,7 @@ -import { EndpointDefinition, PathPart } from "@fern-api/fdr-sdk/api-definition"; import { unknownToString } from "@fern-api/ui-core-utils"; +import type { EndpointDefinition, PathPart } from "./latest"; + function buildQueryParams( queryParameters: Record | undefined ): string { diff --git a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/resources/commons/types/WithDescription.ts b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/resources/commons/types/WithDescription.ts index 60c42aa69d..ea62073ec5 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/resources/commons/types/WithDescription.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/api/resources/latest/resources/commons/types/WithDescription.ts @@ -2,8 +2,6 @@ * This file was auto-generated by Fern from our API Definition. */ -import * as FernRegistry from "../../../../../../../index"; - export interface WithDescription { - description: FernRegistry.docs.latest.MarkdownText | undefined; + description: string | undefined; } diff --git a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MarkdownText.ts b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MarkdownText.ts deleted file mode 100644 index ecc62f953c..0000000000 --- a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MarkdownText.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -import * as FernRegistry from "../../../../../index"; - -export type MarkdownText = string | FernRegistry.docs.latest.ResolvedMdx; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MdxEngine.ts b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MdxEngine.ts deleted file mode 100644 index b8e28b32d2..0000000000 --- a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/MdxEngine.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * This file was auto-generated by Fern from our API Definition. - */ - -/** - * Engine used to render MDX content - */ -export type MdxEngine = - /** - * https://github.com/hashicorp/next-mdx-remote */ - | "next-mdx-remote" - /** - * https://github.com/kentcdodds/mdx-bundler */ - | "mdx-bundler"; - -export const MdxEngine = { - NextMdxRemote: "next-mdx-remote", - MdxBundler: "mdx-bundler", -} as const; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/ResolvedMdx.ts b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/ResolvedMdx.ts index 15e2c951e0..6451883b24 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/ResolvedMdx.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/ResolvedMdx.ts @@ -5,7 +5,6 @@ import * as FernRegistry from "../../../../../index"; export interface ResolvedMdx { - engine: FernRegistry.docs.latest.MdxEngine; code: string; frontmatter: FernRegistry.docs.latest.Frontmatter; scope: Record; diff --git a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/index.ts b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/index.ts index de403a7896..a50b14cd69 100644 --- a/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/index.ts +++ b/packages/fdr-sdk/src/client/generated/api/resources/docs/resources/latest/types/index.ts @@ -1,3 +1 @@ -export * from "./MdxEngine"; export * from "./ResolvedMdx"; -export * from "./MarkdownText"; diff --git a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts index 4ca9b46e2a..4c209019fd 100644 --- a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts +++ b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts @@ -1,7 +1,9 @@ -import assertNever from "@fern-api/ui-core-utils/assertNever"; -import titleCase from "@fern-api/ui-core-utils/titleCase"; import { kebabCase } from "es-toolkit/string"; import isEqual from "fast-deep-equal"; + +import assertNever from "@fern-api/ui-core-utils/assertNever"; +import titleCase from "@fern-api/ui-core-utils/titleCase"; + import { APIV1Db, APIV1Read, APIV1Write, FdrAPI } from "../../client"; import { generateEndpointErrorExample, diff --git a/packages/fdr-sdk/src/converters/db/convertDocsDefinitionToDb.ts b/packages/fdr-sdk/src/converters/db/convertDocsDefinitionToDb.ts index 3a54f742b0..ffaeab39fb 100644 --- a/packages/fdr-sdk/src/converters/db/convertDocsDefinitionToDb.ts +++ b/packages/fdr-sdk/src/converters/db/convertDocsDefinitionToDb.ts @@ -1,5 +1,7 @@ -import assertNever from "@fern-api/ui-core-utils/assertNever"; import { kebabCase } from "es-toolkit/string"; + +import assertNever from "@fern-api/ui-core-utils/assertNever"; + import { FernNavigation } from "../.."; import { DocsV1Db, diff --git a/packages/fdr-sdk/src/converters/db/examples/generateEndpointExampleCall.ts b/packages/fdr-sdk/src/converters/db/examples/generateEndpointExampleCall.ts index 4669fe77e7..a7d0c382df 100644 --- a/packages/fdr-sdk/src/converters/db/examples/generateEndpointExampleCall.ts +++ b/packages/fdr-sdk/src/converters/db/examples/generateEndpointExampleCall.ts @@ -1,11 +1,12 @@ import assertNever from "@fern-api/ui-core-utils/assertNever"; + import { APIV1Write } from "../../../client"; import { + ResolveTypeById, generateExampleFromTypeReference, generateExampleFromTypeShape, generateHttpRequestBodyExample, generateHttpResponseBodyExample, - ResolveTypeById, } from "./generateHttpBodyExample"; const MAX_OPTIONAL_EXAMPLES_FOR_QUERY_PARAMS = 2; diff --git a/packages/fdr-sdk/src/converters/db/examples/generateHttpBodyExample.ts b/packages/fdr-sdk/src/converters/db/examples/generateHttpBodyExample.ts index db7b3aa019..5ae9f1ca79 100644 --- a/packages/fdr-sdk/src/converters/db/examples/generateHttpBodyExample.ts +++ b/packages/fdr-sdk/src/converters/db/examples/generateHttpBodyExample.ts @@ -1,4 +1,5 @@ import assertNever from "@fern-api/ui-core-utils/assertNever"; + import { APIV1Write } from "../../../client"; export type ResolveTypeById = ( diff --git a/packages/fdr-sdk/src/converters/db/upgrade/migrateDocsDbDefinition.ts b/packages/fdr-sdk/src/converters/db/upgrade/migrateDocsDbDefinition.ts index 0e102b4719..4afb3adea4 100644 --- a/packages/fdr-sdk/src/converters/db/upgrade/migrateDocsDbDefinition.ts +++ b/packages/fdr-sdk/src/converters/db/upgrade/migrateDocsDbDefinition.ts @@ -1,4 +1,5 @@ import isPlainObject from "@fern-api/ui-core-utils/isPlainObject"; + import { DocsV1Db } from "../../../client"; import { upgradeV1ToV2 } from "./upgradeV1ToV2"; import { upgradeV2ToV3 } from "./upgradeV2ToV3"; diff --git a/packages/fdr-sdk/src/converters/db/upgrade/upgradeV2ToV3.ts b/packages/fdr-sdk/src/converters/db/upgrade/upgradeV2ToV3.ts index c8a24d20f0..2112f75dcf 100644 --- a/packages/fdr-sdk/src/converters/db/upgrade/upgradeV2ToV3.ts +++ b/packages/fdr-sdk/src/converters/db/upgrade/upgradeV2ToV3.ts @@ -1,4 +1,5 @@ import { mapValues } from "es-toolkit/object"; + import { DocsV1Db } from "../../../client"; export function upgradeV2ToV3( diff --git a/packages/fdr-sdk/src/converters/read/convertDbAPIDefinitionToRead.ts b/packages/fdr-sdk/src/converters/read/convertDbAPIDefinitionToRead.ts index debe32fc62..508d94eda1 100644 --- a/packages/fdr-sdk/src/converters/read/convertDbAPIDefinitionToRead.ts +++ b/packages/fdr-sdk/src/converters/read/convertDbAPIDefinitionToRead.ts @@ -1,4 +1,5 @@ import assertNever from "@fern-api/ui-core-utils/assertNever"; + import { APIV1Db, APIV1Read } from "../../client"; export function convertDbAPIDefinitionsToRead( diff --git a/packages/fdr-sdk/src/converters/read/convertDbDocsConfigToRead.ts b/packages/fdr-sdk/src/converters/read/convertDbDocsConfigToRead.ts index f15ded3b0f..f8fb11ddb7 100644 --- a/packages/fdr-sdk/src/converters/read/convertDbDocsConfigToRead.ts +++ b/packages/fdr-sdk/src/converters/read/convertDbDocsConfigToRead.ts @@ -1,6 +1,8 @@ -import assertNever from "@fern-api/ui-core-utils/assertNever"; import { kebabCase } from "es-toolkit/string"; import tinycolor from "tinycolor2"; + +import assertNever from "@fern-api/ui-core-utils/assertNever"; + import { DocsV1Db, DocsV1Read, diff --git a/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts b/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts index ad09041751..f38a281952 100644 --- a/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts +++ b/packages/fdr-sdk/src/converters/read/convertDocsDefinitionToRead.ts @@ -1,4 +1,5 @@ import { mapValues } from "es-toolkit/object"; + import { APIV1Db, APIV1Read, DocsV1Db, DocsV1Read } from "../../client"; import { SearchInfo } from "../../client/FdrAPI"; import { FernRegistry } from "../../client/generated"; diff --git a/packages/fdr-sdk/src/navigation/ApiTypeIdVisitor.ts b/packages/fdr-sdk/src/navigation/ApiTypeIdVisitor.ts index 7cf40156f0..8a507e8cb1 100644 --- a/packages/fdr-sdk/src/navigation/ApiTypeIdVisitor.ts +++ b/packages/fdr-sdk/src/navigation/ApiTypeIdVisitor.ts @@ -1,5 +1,7 @@ -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { noop } from "ts-essentials"; + +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type { APIV1Read } from "../client/types"; export class ApiTypeIdVisitor { diff --git a/packages/fdr-sdk/src/navigation/NodeCollector.ts b/packages/fdr-sdk/src/navigation/NodeCollector.ts index 6cf950666d..a3f1c03ee9 100644 --- a/packages/fdr-sdk/src/navigation/NodeCollector.ts +++ b/packages/fdr-sdk/src/navigation/NodeCollector.ts @@ -1,5 +1,7 @@ -import { EMPTY_ARRAY } from "@fern-api/ui-core-utils"; import { once } from "es-toolkit/function"; + +import { EMPTY_ARRAY } from "@fern-api/ui-core-utils"; + import { FernNavigation } from "./.."; import { pruneVersionNode } from "./utils/pruneVersionNode"; import { NavigationNodeWithMetadata } from "./versions"; diff --git a/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts b/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts index 9545b18a53..8511a9fba7 100644 --- a/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts +++ b/packages/fdr-sdk/src/navigation/migrators/v1ToV2.ts @@ -1,5 +1,7 @@ -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { UnreachableCaseError } from "ts-essentials"; + +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import { FernNavigation } from "../.."; /** diff --git a/packages/fdr-sdk/src/navigation/utils/createBreadcrumb.ts b/packages/fdr-sdk/src/navigation/utils/createBreadcrumb.ts index 4ab274e326..6f717c36b3 100644 --- a/packages/fdr-sdk/src/navigation/utils/createBreadcrumb.ts +++ b/packages/fdr-sdk/src/navigation/utils/createBreadcrumb.ts @@ -1,5 +1,7 @@ -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { noop } from "ts-essentials"; + +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import { FernNavigation } from "../.."; export function createBreadcrumb( diff --git a/packages/fdr-sdk/src/navigation/utils/deleteChild.ts b/packages/fdr-sdk/src/navigation/utils/deleteChild.ts index 0c329523c0..3c2542f9dc 100644 --- a/packages/fdr-sdk/src/navigation/utils/deleteChild.ts +++ b/packages/fdr-sdk/src/navigation/utils/deleteChild.ts @@ -1,4 +1,5 @@ import { MarkOptional, UnreachableCaseError } from "ts-essentials"; + import { FernNavigation } from "../.."; import { DeleterAction } from "../../utils/traversers/types"; diff --git a/packages/fdr-sdk/src/navigation/utils/findNode.ts b/packages/fdr-sdk/src/navigation/utils/findNode.ts index 4f8fb7c497..1138f900cb 100644 --- a/packages/fdr-sdk/src/navigation/utils/findNode.ts +++ b/packages/fdr-sdk/src/navigation/utils/findNode.ts @@ -1,3 +1,5 @@ +import escapeStringRegexp from "escape-string-regexp"; + import { FernNavigation } from "../.."; import { NodeCollector } from "../NodeCollector"; import { isApiReferenceNode } from "../versions/latest/isApiReferenceNode"; @@ -119,7 +121,10 @@ export function findNode( : undefined; const slugPrefix = currentVersion?.slug ?? root.slug; const unversionedSlug = FernNavigation.Slug( - found.node.slug.replace(new RegExp(`^${slugPrefix}/?`), "") + found.node.slug.replace( + new RegExp(`^${escapeStringRegexp(slugPrefix)}/`), + "" + ) ); return { type: "found", diff --git a/packages/fdr-sdk/src/navigation/utils/followRedirect.ts b/packages/fdr-sdk/src/navigation/utils/followRedirect.ts index 4a05d92997..4dc9e19b70 100644 --- a/packages/fdr-sdk/src/navigation/utils/followRedirect.ts +++ b/packages/fdr-sdk/src/navigation/utils/followRedirect.ts @@ -1,4 +1,5 @@ import { UnreachableCaseError } from "ts-essentials"; + import { FernNavigation } from "../.."; export function followRedirect( diff --git a/packages/fdr-sdk/src/navigation/utils/getApiReferenceId.ts b/packages/fdr-sdk/src/navigation/utils/getApiReferenceId.ts index f6e099dda0..618b02a01c 100644 --- a/packages/fdr-sdk/src/navigation/utils/getApiReferenceId.ts +++ b/packages/fdr-sdk/src/navigation/utils/getApiReferenceId.ts @@ -1,4 +1,5 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import { FernNavigation } from "../.."; const RETURN_UNDEFINED = () => undefined; diff --git a/packages/fdr-sdk/src/navigation/utils/hasChildren.ts b/packages/fdr-sdk/src/navigation/utils/hasChildren.ts index 9804df5265..9a6944b676 100644 --- a/packages/fdr-sdk/src/navigation/utils/hasChildren.ts +++ b/packages/fdr-sdk/src/navigation/utils/hasChildren.ts @@ -1,4 +1,5 @@ import { UnreachableCaseError } from "ts-essentials"; + import { NavigationNodeParent } from "../versions"; export function hasChildren(node: NavigationNodeParent): boolean { diff --git a/packages/fdr-sdk/src/navigation/utils/index.ts b/packages/fdr-sdk/src/navigation/utils/index.ts index 5a595bc090..842cb0056d 100644 --- a/packages/fdr-sdk/src/navigation/utils/index.ts +++ b/packages/fdr-sdk/src/navigation/utils/index.ts @@ -8,3 +8,4 @@ export * from "./toApis"; export * from "./toPages"; export * from "./toRootNode"; export * from "./toUnversionedSlug"; +export * from "./updatePointsTo"; diff --git a/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts b/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts index 03b5831496..4ea57bebcd 100644 --- a/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts +++ b/packages/fdr-sdk/src/navigation/utils/pruneNavigationTree.ts @@ -1,4 +1,5 @@ import structuredClone from "@ungap/structured-clone"; + import { FernNavigation } from "../.."; import { prunetree } from "../../utils/traversers/prunetree"; import { mutableDeleteChild } from "./deleteChild"; diff --git a/packages/fdr-sdk/src/navigation/utils/stringifyEndpointPathParts.ts b/packages/fdr-sdk/src/navigation/utils/stringifyEndpointPathParts.ts index eb3e4eea71..51b8080e7f 100644 --- a/packages/fdr-sdk/src/navigation/utils/stringifyEndpointPathParts.ts +++ b/packages/fdr-sdk/src/navigation/utils/stringifyEndpointPathParts.ts @@ -1,4 +1,5 @@ import urljoin from "url-join"; + import type { APIV1Read } from "../../client/types"; export function stringifyEndpointPathParts( diff --git a/packages/fdr-sdk/src/navigation/utils/toApis.ts b/packages/fdr-sdk/src/navigation/utils/toApis.ts index 8b591cadc5..e315c23ce8 100644 --- a/packages/fdr-sdk/src/navigation/utils/toApis.ts +++ b/packages/fdr-sdk/src/navigation/utils/toApis.ts @@ -1,4 +1,5 @@ import { mapValues } from "es-toolkit/object"; + import { ApiDefinition } from "../.."; import { DocsV2Read } from "../../client"; diff --git a/packages/fdr-sdk/src/navigation/utils/toPages.ts b/packages/fdr-sdk/src/navigation/utils/toPages.ts index 9a7984c336..41572f04da 100644 --- a/packages/fdr-sdk/src/navigation/utils/toPages.ts +++ b/packages/fdr-sdk/src/navigation/utils/toPages.ts @@ -1,4 +1,5 @@ import { mapValues } from "es-toolkit/object"; + import { DocsV2Read } from "../../client"; export function toPages(docs: DocsV2Read.LoadDocsForUrlResponse) { diff --git a/packages/fdr-sdk/src/navigation/utils/toUnversionedSlug.ts b/packages/fdr-sdk/src/navigation/utils/toUnversionedSlug.ts index be4ca19f14..743a4a804a 100644 --- a/packages/fdr-sdk/src/navigation/utils/toUnversionedSlug.ts +++ b/packages/fdr-sdk/src/navigation/utils/toUnversionedSlug.ts @@ -1,3 +1,5 @@ +import escapeStringRegexp from "escape-string-regexp"; + import { Slug } from ".."; /** @@ -6,5 +8,7 @@ import { Slug } from ".."; * For example, if the original slug is "docs/v1.0.0/foo/bar", the unversionedSlug is "foo/bar". */ export function toUnversionedSlug(slug: Slug, versionSlug: Slug): Slug { - return Slug(slug.replace(new RegExp(`^${versionSlug}(/|$)`), "")); + return Slug( + slug.replace(new RegExp(`^${escapeStringRegexp(versionSlug)}(/|$)`), "") + ); } diff --git a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeLeaf.ts b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeLeaf.ts index fea99f4296..a59a1f8ac9 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeLeaf.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeLeaf.ts @@ -1,9 +1,9 @@ import { LinkNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - isMarkdownLeaf, type NavigationNodeMarkdownLeaf, + isMarkdownLeaf, } from "./NavigationNodePageLeaf"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeMarkdown.ts b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeMarkdown.ts index 2bc683b1d8..6192cd811a 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeMarkdown.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeMarkdown.ts @@ -1,11 +1,11 @@ import type { NavigationNode } from "./NavigationNode"; import { - isMarkdownLeaf, type NavigationNodeMarkdownLeaf, + isMarkdownLeaf, } from "./NavigationNodePageLeaf"; import { - isSectionOverview, type NavigationNodeSectionOverview, + isSectionOverview, } from "./NavigationNodeSectionOverview"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeNeighbor.ts b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeNeighbor.ts index 7c85060ed7..81ed7a3814 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeNeighbor.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeNeighbor.ts @@ -1,9 +1,9 @@ import type { ChangelogEntryNode, ChangelogNode, PageNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - isSectionOverview, type NavigationNodeSectionOverview, + isSectionOverview, } from "./NavigationNodeSectionOverview"; export type NavigationNodeNeighbor = diff --git a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodePage.ts b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodePage.ts index 2a5afff60c..6da746315c 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodePage.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodePage.ts @@ -1,9 +1,9 @@ import type { ChangelogNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - hasMarkdown, type NavigationNodeWithMarkdown, + hasMarkdown, } from "./NavigationNodeMarkdown"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeSectionOverview.ts b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeSectionOverview.ts index f4bbac8c3f..e3bb63d5aa 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeSectionOverview.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/NavigationNodeSectionOverview.ts @@ -1,6 +1,6 @@ import type { PageId, WithOverviewPage } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isSection, type NavigationNodeSection } from "./NavigationNodeSection"; +import { type NavigationNodeSection, isSection } from "./NavigationNodeSection"; type WithRequiredOverviewPage = T & { overviewPageId: PageId; diff --git a/packages/fdr-sdk/src/navigation/versions/latest/getChildren.ts b/packages/fdr-sdk/src/navigation/versions/latest/getChildren.ts index dea2f84702..c712fb04f8 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/getChildren.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/getChildren.ts @@ -1,4 +1,5 @@ import { UnreachableCaseError } from "ts-essentials"; + import { NavigationNode } from "./NavigationNode"; import { isLeaf } from "./NavigationNodeLeaf"; diff --git a/packages/fdr-sdk/src/navigation/versions/latest/getPageId.ts b/packages/fdr-sdk/src/navigation/versions/latest/getPageId.ts index 1b62270eb9..a2406ed9e5 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/getPageId.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/getPageId.ts @@ -1,4 +1,5 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type { NavigationNodePage, PageId } from "."; const RETURN_PAGEID = (node: { pageId: PageId }) => node.pageId; diff --git a/packages/fdr-sdk/src/navigation/versions/latest/slugjoin.ts b/packages/fdr-sdk/src/navigation/versions/latest/slugjoin.ts index 8640efe829..6b5327fb63 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/slugjoin.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/slugjoin.ts @@ -1,5 +1,7 @@ -import { isNonNullish } from "@fern-api/ui-core-utils"; import urljoin from "url-join"; + +import { isNonNullish } from "@fern-api/ui-core-utils"; + import { Slug } from "."; // normalizes slug parts and joins them with a single slash @@ -10,7 +12,8 @@ export function slugjoin( .filter(isNonNullish) .flatMap((part) => typeof part === "string" ? [part.trim()] : part.map((part) => part.trim()) - ); + ) + .map((part) => decodeURIComponent(part)); return Slug( urljoin(slugArray) .replaceAll("//*", "/") diff --git a/packages/fdr-sdk/src/navigation/versions/latest/toDefaultSlug.ts b/packages/fdr-sdk/src/navigation/versions/latest/toDefaultSlug.ts index 0ce6567a5e..7f48bca829 100644 --- a/packages/fdr-sdk/src/navigation/versions/latest/toDefaultSlug.ts +++ b/packages/fdr-sdk/src/navigation/versions/latest/toDefaultSlug.ts @@ -1,3 +1,5 @@ +import escapeStringRegexp from "escape-string-regexp"; + import { Slug } from "."; /** @@ -26,7 +28,7 @@ export function toDefaultSlug( if (slug.startsWith(versionSlug)) { return Slug( slug - .replace(new RegExp(`^${versionSlug.replaceAll("/", "\\/")}`), rootSlug) + .replace(new RegExp(`^${escapeStringRegexp(versionSlug)}`), rootSlug) .replace(/^\//, "") ); } diff --git a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeLeaf.ts b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeLeaf.ts index fea99f4296..a59a1f8ac9 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeLeaf.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeLeaf.ts @@ -1,9 +1,9 @@ import { LinkNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - isMarkdownLeaf, type NavigationNodeMarkdownLeaf, + isMarkdownLeaf, } from "./NavigationNodePageLeaf"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeMarkdown.ts b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeMarkdown.ts index 2bc683b1d8..6192cd811a 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeMarkdown.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeMarkdown.ts @@ -1,11 +1,11 @@ import type { NavigationNode } from "./NavigationNode"; import { - isMarkdownLeaf, type NavigationNodeMarkdownLeaf, + isMarkdownLeaf, } from "./NavigationNodePageLeaf"; import { - isSectionOverview, type NavigationNodeSectionOverview, + isSectionOverview, } from "./NavigationNodeSectionOverview"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeNeighbor.ts b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeNeighbor.ts index 7c85060ed7..81ed7a3814 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeNeighbor.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeNeighbor.ts @@ -1,9 +1,9 @@ import type { ChangelogEntryNode, ChangelogNode, PageNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - isSectionOverview, type NavigationNodeSectionOverview, + isSectionOverview, } from "./NavigationNodeSectionOverview"; export type NavigationNodeNeighbor = diff --git a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodePage.ts b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodePage.ts index 34d8a43a28..94ae669b02 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodePage.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodePage.ts @@ -1,9 +1,9 @@ import type { ChangelogMonthNode, ChangelogNode, ChangelogYearNode } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isApiLeaf, type NavigationNodeApiLeaf } from "./NavigationNodeApiLeaf"; +import { type NavigationNodeApiLeaf, isApiLeaf } from "./NavigationNodeApiLeaf"; import { - hasMarkdown, type NavigationNodeWithMarkdown, + hasMarkdown, } from "./NavigationNodeMarkdown"; /** diff --git a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeSectionOverview.ts b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeSectionOverview.ts index f4bbac8c3f..e3bb63d5aa 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeSectionOverview.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/NavigationNodeSectionOverview.ts @@ -1,6 +1,6 @@ import type { PageId, WithOverviewPage } from "."; import type { NavigationNode } from "./NavigationNode"; -import { isSection, type NavigationNodeSection } from "./NavigationNodeSection"; +import { type NavigationNodeSection, isSection } from "./NavigationNodeSection"; type WithRequiredOverviewPage = T & { overviewPageId: PageId; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/convertAvailability.ts b/packages/fdr-sdk/src/navigation/versions/v1/convertAvailability.ts index a44a096502..3d1496fe99 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/convertAvailability.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/convertAvailability.ts @@ -1,4 +1,5 @@ import { UnreachableCaseError } from "ts-essentials"; + import { FernNavigation } from "../../.."; import { FdrAPI } from "../../../client/types"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/converters/ApiReferenceNavigationConverter.ts b/packages/fdr-sdk/src/navigation/versions/v1/converters/ApiReferenceNavigationConverter.ts index 4005d056bc..b6fd6ce3a7 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/converters/ApiReferenceNavigationConverter.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/converters/ApiReferenceNavigationConverter.ts @@ -1,7 +1,9 @@ -import titleCase from "@fern-api/ui-core-utils/titleCase"; -import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; import { noop } from "ts-essentials"; import urljoin from "url-join"; + +import titleCase from "@fern-api/ui-core-utils/titleCase"; +import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import { FernNavigation } from "../../../.."; import { APIV1Read, DocsV1Read } from "../../../../client/types"; import { ApiDefinitionHolder } from "../../../ApiDefinitionHolder"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/converters/ChangelogConverter.ts b/packages/fdr-sdk/src/navigation/versions/v1/converters/ChangelogConverter.ts index 4eb01a7973..6c35d67d47 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/converters/ChangelogConverter.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/converters/ChangelogConverter.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; + import { FernNavigation } from "../../../.."; import type { DocsV1Read } from "../../../../client/types"; import { NodeIdGenerator } from "./NodeIdGenerator"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/converters/NavigationConfigConverter.ts b/packages/fdr-sdk/src/navigation/versions/v1/converters/NavigationConfigConverter.ts index 0a8d79e8fb..cb00d5a3c5 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/converters/NavigationConfigConverter.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/converters/NavigationConfigConverter.ts @@ -1,6 +1,8 @@ +import { kebabCase } from "es-toolkit/string"; + import assertNever from "@fern-api/ui-core-utils/assertNever"; import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; -import { kebabCase } from "es-toolkit/string"; + import { FernNavigation } from "../../../.."; import type { APIV1Read, DocsV1Read } from "../../../../client/types"; import { diff --git a/packages/fdr-sdk/src/navigation/versions/v1/converters/__test__/groupByTime.test.ts b/packages/fdr-sdk/src/navigation/versions/v1/converters/__test__/groupByTime.test.ts index bc702dd686..94c16fa4c5 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/converters/__test__/groupByTime.test.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/converters/__test__/groupByTime.test.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; + import { FernNavigation } from "../../../../.."; import { groupByMonth, groupByYear } from "../ChangelogConverter"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/converters/toRootNode.ts b/packages/fdr-sdk/src/navigation/versions/v1/converters/toRootNode.ts index 23de59f5fa..698e06dccd 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/converters/toRootNode.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/converters/toRootNode.ts @@ -1,4 +1,5 @@ import { mapValues } from "es-toolkit/object"; + import { FernNavigation } from "../../../.."; import { APIV1Read, type DocsV2Read } from "../../../../client/types"; import { getFrontmatter } from "../../../utils/getFrontmatter"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/followRedirect.ts b/packages/fdr-sdk/src/navigation/versions/v1/followRedirect.ts index 91e117d967..2983f46a1e 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/followRedirect.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/followRedirect.ts @@ -1,4 +1,5 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import { FernNavigation } from "../../.."; import { hasMetadata } from "./NavigationNodeWithMetadata"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/getChildren.ts b/packages/fdr-sdk/src/navigation/versions/v1/getChildren.ts index dea2f84702..c712fb04f8 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/getChildren.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/getChildren.ts @@ -1,4 +1,5 @@ import { UnreachableCaseError } from "ts-essentials"; + import { NavigationNode } from "./NavigationNode"; import { isLeaf } from "./NavigationNodeLeaf"; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/getPageId.ts b/packages/fdr-sdk/src/navigation/versions/v1/getPageId.ts index 4cb25162cb..5eac265e94 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/getPageId.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/getPageId.ts @@ -1,4 +1,5 @@ import visitDiscriminatedUnion from "@fern-api/ui-core-utils/visitDiscriminatedUnion"; + import type { NavigationNodePage, PageId } from "."; const RETURN_PAGEID = (node: { pageId: PageId }) => node.pageId; diff --git a/packages/fdr-sdk/src/navigation/versions/v1/slugjoin.ts b/packages/fdr-sdk/src/navigation/versions/v1/slugjoin.ts index 2d4e5c7a84..5b28f833b3 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/slugjoin.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/slugjoin.ts @@ -1,4 +1,5 @@ import urljoin from "url-join"; + import { Slug } from "."; // normalizes slug parts and joins them with a single slash diff --git a/packages/fdr-sdk/src/navigation/versions/v1/toDefaultSlug.ts b/packages/fdr-sdk/src/navigation/versions/v1/toDefaultSlug.ts index 0ce6567a5e..7f48bca829 100644 --- a/packages/fdr-sdk/src/navigation/versions/v1/toDefaultSlug.ts +++ b/packages/fdr-sdk/src/navigation/versions/v1/toDefaultSlug.ts @@ -1,3 +1,5 @@ +import escapeStringRegexp from "escape-string-regexp"; + import { Slug } from "."; /** @@ -26,7 +28,7 @@ export function toDefaultSlug( if (slug.startsWith(versionSlug)) { return Slug( slug - .replace(new RegExp(`^${versionSlug.replaceAll("/", "\\/")}`), rootSlug) + .replace(new RegExp(`^${escapeStringRegexp(versionSlug)}`), rootSlug) .replace(/^\//, "") ); } diff --git a/packages/fdr-sdk/src/utils/traversers/bfs.ts b/packages/fdr-sdk/src/utils/traversers/bfs.ts index 984ea66271..c0d8e804b5 100644 --- a/packages/fdr-sdk/src/utils/traversers/bfs.ts +++ b/packages/fdr-sdk/src/utils/traversers/bfs.ts @@ -8,9 +8,11 @@ export function bfs( ): void { const queue: [N, P[]][] = [[root, []]]; while (queue.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const [node, parents] = queue.shift()!; - const next = visit(node, parents); + const [node, parents] = queue.shift() ?? []; + if (!node) { + continue; + } + const next = visit(node, parents ?? []); if (next === SKIP) { continue; @@ -20,7 +22,7 @@ export function bfs( if (isParent(node)) { for (const child of [...getChildren(node)]) { - queue.push([child, [...parents, node]]); + queue.push([child, [...(parents ?? []), node]]); } } } diff --git a/packages/fdr-sdk/src/utils/traversers/dfs.ts b/packages/fdr-sdk/src/utils/traversers/dfs.ts index 908702a48b..25f2dc188c 100644 --- a/packages/fdr-sdk/src/utils/traversers/dfs.ts +++ b/packages/fdr-sdk/src/utils/traversers/dfs.ts @@ -8,9 +8,12 @@ export function dfs( ): void { const stack: [N, P[]][] = [[root, []]]; while (stack.length > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const [node, parents] = stack.pop()!; - const next = visit(node, parents); + const [node, parents] = stack.pop() ?? []; + if (!node) { + continue; + } + const next = visit(node, parents ?? []); + if (next === SKIP) { continue; } else if (next === STOP) { @@ -19,7 +22,7 @@ export function dfs( if (isParent(node)) { for (const child of [...getChildren(node)].reverse()) { - stack.push([child, [...parents, node]]); + stack.push([child, [...(parents ?? []), node]]); } } } diff --git a/packages/fern-docs/auth/package.json b/packages/fern-docs/auth/package.json index bb8abbdd14..46c01d840d 100644 --- a/packages/fern-docs/auth/package.json +++ b/packages/fern-docs/auth/package.json @@ -43,6 +43,6 @@ "prettier": "^3.4.2", "stylelint": "^16.1.0", "typescript": "^5", - "vitest": "^2.1.4" + "vitest": "^3.0.5" } } diff --git a/packages/fern-docs/bundle/.depcheckrc.json b/packages/fern-docs/bundle/.depcheckrc.json index 9575e03359..003bef6c0a 100644 --- a/packages/fern-docs/bundle/.depcheckrc.json +++ b/packages/fern-docs/bundle/.depcheckrc.json @@ -1,24 +1,21 @@ { "ignores": [ - "@fern-platform/configs", - "@types/node", - "@types/react", - "@types/react-dom", - "autoprefixer", - "postcss", - "postcss-import", + "@chromatic-com/storybook", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "@storybook/addon-links", + "@storybook/addon-onboarding", + "@storybook/blocks", + "@storybook/test", + "@tailwindcss/postcss", "@tailwindcss/typography", - "@tailwindcss/forms", - "tailwindcss", - "jsonpath", + "@upstash/qstash", "cssnano", - "sharp", "esbuild", "glslify-import", "glslify-loader", "raw-loader", - "@upstash/qstash", - "@mdx-js/react" + "tailwindcss" ], "ignore-patterns": ["dist"] } diff --git a/packages/fern-docs/ui/.storybook/main.ts b/packages/fern-docs/bundle/.storybook/main.ts similarity index 99% rename from packages/fern-docs/ui/.storybook/main.ts rename to packages/fern-docs/bundle/.storybook/main.ts index aa7d50b8da..062d3f44e1 100644 --- a/packages/fern-docs/ui/.storybook/main.ts +++ b/packages/fern-docs/bundle/.storybook/main.ts @@ -1,5 +1,4 @@ import type { StorybookConfig } from "@storybook/nextjs"; - import { dirname, join } from "path"; /** diff --git a/packages/fern-docs/ui/.storybook/preview.tsx b/packages/fern-docs/bundle/.storybook/preview.tsx similarity index 77% rename from packages/fern-docs/ui/.storybook/preview.tsx rename to packages/fern-docs/bundle/.storybook/preview.tsx index 97c4ad98d6..a2078ffd6c 100644 --- a/packages/fern-docs/ui/.storybook/preview.tsx +++ b/packages/fern-docs/bundle/.storybook/preview.tsx @@ -1,16 +1,19 @@ -import { FernTooltipProvider, Toaster } from "@fern-docs/components"; +import React from "react"; + import { withThemeByClassName } from "@storybook/addon-themes"; -import type { Preview } from "@storybook/react"; +import type { Decorator, Preview } from "@storybook/react"; + +import { FernTooltipProvider, Toaster } from "@fern-docs/components"; + import "../src/css/globals.scss"; -import "./variables.css"; -const globalDecorator = (Story) => ( +const globalDecorator = (Story: any) => ( ); -export const decorators = [ +export const decorators: Decorator[] = [ globalDecorator, withThemeByClassName({ themes: { diff --git a/packages/fern-docs/bundle/next.config.mjs b/packages/fern-docs/bundle/next.config.ts similarity index 74% rename from packages/fern-docs/bundle/next.config.mjs rename to packages/fern-docs/bundle/next.config.ts index 832cd566ef..900debde4c 100644 --- a/packages/fern-docs/bundle/next.config.mjs +++ b/packages/fern-docs/bundle/next.config.ts @@ -1,5 +1,7 @@ +import { NextConfig } from "next"; +import { PHASE_DEVELOPMENT_SERVER } from "next/constants"; + import NextBundleAnalyzer from "@next/bundle-analyzer"; -import { PHASE_DEVELOPMENT_SERVER } from "next/constants.js"; import process from "node:process"; const cdnUri = @@ -10,46 +12,20 @@ const isTrailingSlashEnabled = process.env.TRAILING_SLASH === "1" || process.env.NEXT_PUBLIC_TRAILING_SLASH === "1"; -// TODO: move this to a shared location (this is copied in @fern-docs/ui FernImage.tsx) -const DOCS_FILES_ALLOWLIST = [ - { - protocol: "https", - hostname: "fdr-prod-docs-files.s3.us-east-1.amazonaws.com", - port: "", - }, - { - protocol: "https", - hostname: "fdr-prod-docs-files-public.s3.amazonaws.com", - port: "", - }, - { - protocol: "https", - hostname: "fdr-dev2-docs-files.s3.us-east-1.amazonaws.com", - port: "", - }, - { - protocol: "https", - hostname: "fdr-dev2-docs-files-public.s3.amazonaws.com", - port: "", - }, - { - protocol: "https", - hostname: "files.buildwithfern.com", - port: "", - }, - { - protocol: "https", - hostname: "files-dev2.buildwithfern.com", - port: "", - }, +// TODO: move this to a shared location (this is copied in FernImage.tsx) +const NEXT_IMAGE_HOSTS = [ + "fdr-prod-docs-files.s3.us-east-1.amazonaws.com", + "fdr-prod-docs-files-public.s3.amazonaws.com", + "fdr-dev2-docs-files.s3.us-east-1.amazonaws.com", + "fdr-dev2-docs-files-public.s3.amazonaws.com", + "files.buildwithfern.com", + "files-dev2.buildwithfern.com", ]; -/** @type {import("next").NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { reactStrictMode: true, trailingSlash: isTrailingSlashEnabled, transpilePackages: [ - "next-mdx-remote", "esbuild", "es-toolkit", "three", @@ -63,16 +39,12 @@ const nextConfig = { "@fern-api/template-resolver", "@fern-api/ui-core-utils", "@fern-docs/auth", - "@fern-docs/cache", "@fern-docs/components", "@fern-docs/edge-config", "@fern-docs/mdx", - "@fern-docs/seo", "@fern-docs/search-server", "@fern-docs/search-ui", - "@fern-docs/search-utils", "@fern-docs/syntax-highlighter", - "@fern-docs/ui", "@fern-docs/utils", "@fern-platform/fdr-utils", "@fern-ui/loadable", @@ -80,27 +52,30 @@ const nextConfig = { ], experimental: { scrollRestoration: true, - instrumentationHook: true, - hardNavigate404: true, optimizePackageImports: [ "@fern-api/fdr-sdk", - "@fern-docs/ui", "@fern-docs/mdx", "@fern-docs/components", "@fern-docs/search-server", "es-toolkit", "ts-essentials", + "lucide-react", ], - - /** - * If the rewrite comes from another nextjs middleware, - * x-nextjs-rewrite gets set to the external URL, and then nextjs will try to redirect to that URL. - * - * This flag will prevent nextjs from redirecting to the external URL. - * - * NOTE: @fern-api/next uses this flag to prevent the client from throwing an error. - */ - externalMiddlewareRewritesResolve: true, + optimizeServerReact: Boolean(process.env.VERCEL), + // typedEnv: true, + newDevOverlay: true, + authInterrupts: true, + swcTraceProfiling: true, + webpackBuildWorker: true, + parallelServerCompiles: true, + parallelServerBuildTraces: true, + webpackMemoryOptimizations: true, + taint: true, + useCache: true, + staleTimes: { + dynamic: 180, + static: 180, + }, }, skipMiddlewareUrlNormalize: true, @@ -121,6 +96,19 @@ const nextConfig = { * Note that local development should not set the CDN_URI to ensure that the assets are served from the local server. */ assetPrefix: cdnUri != null ? cdnUri.href : undefined, + typescript: { ignoreBuildErrors: true }, + compiler: { + removeConsole: + process.env.VERCEL_ENV === "production" ? { exclude: ["error"] } : false, + styledJsx: true, + }, + logging: { + fetches: { + fullUrl: true, + hmrRefreshes: true, + }, + incomingRequests: true, + }, headers: async () => { const AccessControlHeaders = [ { @@ -184,7 +172,10 @@ const nextConfig = { ]; }, images: { - remotePatterns: DOCS_FILES_ALLOWLIST, + remotePatterns: NEXT_IMAGE_HOSTS.map((host) => ({ + protocol: "https", + hostname: host, + })), path: cdnUri != null ? `${cdnUri.href}_next/image` : undefined, }, webpack: (config, { isServer }) => { @@ -213,16 +204,18 @@ const nextConfig = { }, }; -function withVercelEnv(config) { +function withVercelEnv(config: NextConfig): NextConfig { return { ...config, - deploymentId: process.env.VERCEL_DEPLOYMENT_ID ?? "dpl_development", // skew protection - productionBrowserSourceMaps: process.env.VERCEL_ENV === "preview", + deploymentId: process.env.VERCEL_DEPLOYMENT_ID, // skew protection + // productionBrowserSourceMaps: process.env.VERCEL_ENV !== "production", + // reactProductionProfiling: process.env.VERCEL_ENV !== "production", + productionBrowserSourceMaps: false, + reactProductionProfiling: false, }; } -/** @type {import("next").NextConfig} */ -export default (phase) => { +export default (phase: string): NextConfig => { const isDev = phase === PHASE_DEVELOPMENT_SERVER; /** diff --git a/packages/fern-docs/bundle/package.json b/packages/fern-docs/bundle/package.json index dfc5e9fcb3..74de7c350a 100644 --- a/packages/fern-docs/bundle/package.json +++ b/packages/fern-docs/bundle/package.json @@ -12,9 +12,13 @@ "crypto": false }, "scripts": { + "build-storybook": "storybook build", + "check-types": "tsc --noEmit", + "chromatic": "pnpx chromatic --project-token=chpt_48b3c560025e978", "depcheck": "depcheck", "docs:build": "next build", - "docs:dev": "NODE_OPTIONS='--inspect' next dev", + "docs:dev": "next dev", + "docs:dev:inspect": "NODE_OPTIONS='--inspect' next dev", "docs:start": "next start", "format": "prettier --write --ignore-unknown \"**\"", "format:check": "prettier --check --ignore-unknown \"**\"", @@ -23,90 +27,157 @@ "lint:eslint:fix": "pnpm lint:eslint --fix", "lint:style": "stylelint 'src/**/*.scss' --allow-empty-input --max-warnings 0", "lint:style:fix": "pnpm lint:style --fix", + "storybook": "storybook dev -p 6006", "test": "vitest --run --passWithNoTests --globals" }, "dependencies": { - "@ai-sdk/amazon-bedrock": "^1.0.6", - "@ai-sdk/anthropic": "^1.0.5", - "@ai-sdk/openai": "^1.0.8", - "@algolia/client-search": "^5.15.0", - "@aws-sdk/client-s3": "^3.685.0", - "@aws-sdk/s3-request-presigner": "^3.685.0", + "@ai-sdk/amazon-bedrock": "^1.1.6", + "@ai-sdk/anthropic": "^1.1.6", + "@ai-sdk/openai": "^1.1.9", + "@algolia/client-search": "^5.20.1", + "@aws-sdk/client-s3": "^3.744.0", + "@aws-sdk/s3-request-presigner": "^3.744.0", "@fern-api/fdr-sdk": "workspace:*", + "@fern-api/template-resolver": "workspace:*", "@fern-api/ui-core-utils": "workspace:*", "@fern-docs/auth": "workspace:*", - "@fern-docs/cache": "workspace:*", "@fern-docs/components": "workspace:*", "@fern-docs/edge-config": "workspace:*", "@fern-docs/mdx": "workspace:*", "@fern-docs/search-server": "workspace:*", "@fern-docs/search-ui": "workspace:*", - "@fern-docs/search-utils": "workspace:*", - "@fern-docs/seo": "workspace:*", "@fern-docs/syntax-highlighter": "workspace:*", - "@fern-docs/ui": "workspace:*", "@fern-docs/utils": "workspace:*", "@fern-fern/fern-docs-sdk": "0.0.5", "@fern-platform/fdr-utils": "workspace:*", + "@fern-ui/loadable": "workspace:*", + "@fern-ui/react-commons": "workspace:*", "@launchdarkly/node-server-sdk": "^9.7.2", "@mdx-js/react": "^3.1.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.57.0", "@opentelemetry/instrumentation": "^0.57.0", "@opentelemetry/sdk-logs": "^0.57.0", + "@radix-ui/colors": "^3.0.0", + "@radix-ui/primitive": "^1.1.1", + "@radix-ui/react-accordion": "^1.2.3", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@react-three/drei": "10.0.0-rc.2", + "@react-three/fiber": "9.0.0-rc.7", + "@segment/snippet": "^5.2.1", "@types/qs": "6.9.14", "@upstash/qstash": "^2.7.16", + "@vercel/functions": "^2.0.0", "@vercel/kv": "^2.0.0", - "@vercel/otel": "^1.10.0", + "@vercel/otel": "^1.10.1", "@workos-inc/node": "^7.31.0", "ai": "^4.0.18", - "algoliasearch": "^5.13.0", + "algoliasearch": "^5.20.1", + "bezier-easing": "^2.1.0", "braintrust": "^0.0.182", + "colorjs.io": "^0.5.2", "cssnano": "^6.0.3", + "date-fns": "^4.1.0", "es-toolkit": "^1.27.0", - "esbuild": "0.24.2", + "esbuild": "0.25.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fastdom": "^1.0.12", "feed": "^4.2.2", - "iron-session": "^8.0.3", - "jose": "^5.2.3", - "jsonpath": "^1.1.1", - "next": "npm:@fern-api/next@14.2.9-fork.2", - "postcss-import": "^16.0.1", - "posthog-node": "^4.2.1", - "qs": "6.12.0", - "react": "^18", - "react-dom": "^18", - "sharp": "^0.33.3", + "github-slugger": "^2.0.0", + "hast-util-properties-to-mdx-jsx-attributes": "^1.0.0", + "hast-util-to-estree": "^3.1.1", + "iconoir-react": "^7.7.0", + "iron-session": "^8.0.4", + "jose": "^5.9.6", + "jotai": "^2.12.0", + "jotai-effect": "^1.1.6", + "jsonpath-plus": "10.0.7", + "launchdarkly-react-client-sdk": "^3.6.0", + "lucide-react": "^0.460.0", + "mdx-bundler": "^10.1.0", + "mermaid": "^11.2.1", + "motion": "^12.4.2", + "next": "15.2.0-canary.64", + "next-themes": "^0.4.4", + "parse-numeric-range": "^1.3.0", + "posthog-js": "^1.216.1", + "posthog-node": "^4.5.0", + "pretty-bytes": "^6.1.1", + "qs": "6.14.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-error-boundary": "^5.0.0", + "react-medium-image-zoom": "^5.2.13", + "react-virtuoso": "^4.12.5", + "rehype-katex": "^7.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gemoji": "^8.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "remark-mdx-frontmatter": "^5.0.0", + "remark-smartypants": "^3.0.2", + "remark-squeeze-paragraphs": "^6.0.0", + "selection-popover": "^0.3.0", + "server-only": "^0.0.1", + "styled-jsx": "^5.1.2", + "swr": "^2.3.2", + "three": "^0.173.0", + "tinycolor2": "^1.6.0", "ts-essentials": "^10.0.1", "url-join": "5.0.0", + "use-memo-one": "^1.1.3", "webflow-api": "^2.4.2", - "zod": "^3.24.1" + "webm-duration-fix": "^1.0.4", + "yaml": "^2.7.0", + "zod": "^3.24.1", + "zustand": "^5.0.2" }, "devDependencies": { + "@chromatic-com/storybook": "^3.2.4", + "@fern-docs/auth": "workspace:*", "@fern-platform/configs": "workspace:*", - "@next/bundle-analyzer": "14.2.9", - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.10", + "@next/bundle-analyzer": "15.2.0-canary.64", + "@storybook/addon-essentials": "^8.5.6", + "@storybook/addon-interactions": "^8.5.6", + "@storybook/addon-links": "^8.5.6", + "@storybook/addon-onboarding": "^8.5.6", + "@storybook/addon-themes": "^8.5.6", + "@storybook/blocks": "^8.5.6", + "@storybook/nextjs": "^8.5.6", + "@storybook/react": "^8.5.6", + "@storybook/test": "^8.5.6", + "@tailwindcss/postcss": "^4.0.6", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.3.1", "@types/node": "^18.7.18", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.4.16", + "@types/react": "19.0.8", + "@types/react-dom": "19.0.3", + "@types/three": "^0.173.0", + "@types/tinycolor2": "^1.4.6", + "chromatic": "^11.3.0", "depcheck": "^1.4.7", "eslint": "^9", "glslify-import": "^3.1.0", "glslify-loader": "^2.0.0", - "postcss": "^8.4.33", + "jsdom": "^24.0.0", "prettier": "^3.4.2", "raw-loader": "^4.0.2", "sass": "^1.74.1", + "storybook": "^8.5.6", "stylelint": "^16.1.0", - "tailwindcss": "^3", + "tailwindcss": "^4.0.6", + "ts-essentials": "^10.0.1", "typescript": "^5", - "vitest": "^2.1.4" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.9", - "@next/swc-darwin-x64": "14.2.9", - "@next/swc-linux-x64-gnu": "14.2.9", - "@next/swc-win32-x64-msvc": "14.2.9" + "vitest": "^3.0.5" } } diff --git a/packages/fern-docs/bundle/postcss.config.js b/packages/fern-docs/bundle/postcss.config.mjs similarity index 57% rename from packages/fern-docs/bundle/postcss.config.js rename to packages/fern-docs/bundle/postcss.config.mjs index b22bcf66ca..136575c6e1 100644 --- a/packages/fern-docs/bundle/postcss.config.js +++ b/packages/fern-docs/bundle/postcss.config.mjs @@ -1,10 +1,7 @@ -module.exports = { +export default { // This is duplicated, make sure to keep it in sync with others plugins: { - "postcss-import": {}, - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, + "@tailwindcss/postcss": {}, ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}), }, }; diff --git a/packages/fern-docs/bundle/src/app/[[...slug]]/route.ts b/packages/fern-docs/bundle/src/app/[[...slug]]/route.ts deleted file mode 100644 index b94279ded3..0000000000 --- a/packages/fern-docs/bundle/src/app/[[...slug]]/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MARKDOWN_PATTERN, RSS_PATTERN } from "@/server/patterns"; -import { notFound } from "next/navigation"; -import { NextRequest, NextResponse } from "next/server"; -import { handleChangelog } from "./changelog"; -import { handleLLMSFullTxt } from "./llms-full.txt"; -import { handleLLMSTxt } from "./llms.txt"; -import { handleMarkdown } from "./markdown"; - -export async function GET( - req: NextRequest, - { params }: { params: { slug?: string[] } } -): Promise { - const slug = params.slug ?? []; - const lastSlug = slug[slug.length - 1]; - console.log(params); - - if (!lastSlug) { - notFound(); - } - - if (lastSlug === "llms.txt") { - return handleLLMSTxt(req, { - params: { slug: params.slug?.slice(0, -1) }, - }); - } - - if (lastSlug === "llms-full.txt") { - return handleLLMSFullTxt(req, { - params: { slug: params.slug?.slice(0, -1) }, - }); - } - - if (lastSlug.match(MARKDOWN_PATTERN)) { - return handleMarkdown(req, { - params: { - slug: [...slug.slice(0, -1), lastSlug.replace(MARKDOWN_PATTERN, "")], - }, - }); - } - - const match = lastSlug.match(RSS_PATTERN); - if (match) { - return handleChangelog(req, { - params: { - slug: [...slug.slice(0, -1), lastSlug.replace(RSS_PATTERN, "")], - format: match[1] as "rss" | "atom" | "json" | undefined, - }, - }); - } - - return notFound(); -} - -export async function OPTIONS(): Promise { - return new NextResponse(null, { - status: 200, - headers: { - "X-Robots-Tag": "noindex", - Allow: "OPTIONS, GET", - }, - }); -} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/api-key-injection/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/api-key-injection/route.ts similarity index 96% rename from packages/fern-docs/bundle/src/app/api/fern-docs/auth/api-key-injection/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/api-key-injection/route.ts index 4e715fcb50..f0e4173971 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/api-key-injection/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/api-key-injection/route.ts @@ -1,18 +1,22 @@ -import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; -import { OryOAuth2Client, getOryAuthorizationUrl } from "@/server/auth/ory"; -import { getReturnToQueryParam } from "@/server/auth/return-to"; -import { withSecureCookie } from "@/server/auth/with-secure-cookie"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { APIKeyInjectionConfig, OryAccessTokenSchema } from "@fern-docs/auth"; -import { getAuthEdgeConfig } from "@fern-docs/edge-config"; -import { COOKIE_FERN_TOKEN, removeTrailingSlash } from "@fern-docs/utils"; import { cookies } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; + import urlJoin from "url-join"; import { WebflowClient } from "webflow-api"; import type { OauthScope } from "webflow-api/api/types/OAuthScope"; +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { APIKeyInjectionConfig, OryAccessTokenSchema } from "@fern-docs/auth"; +import { getAuthEdgeConfig } from "@fern-docs/edge-config"; +import { removeTrailingSlash } from "@fern-docs/utils"; + +import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; +import { OryOAuth2Client, getOryAuthorizationUrl } from "@/server/auth/ory"; +import { getReturnToQueryParam } from "@/server/auth/return-to"; +import { withSecureCookie } from "@/server/auth/with-secure-cookie"; +import { fernToken_admin } from "@/server/env-variables"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; + export const runtime = "edge"; export async function GET( @@ -20,13 +24,13 @@ export async function GET( ): Promise> { const domain = getDocsDomainEdge(req); const host = getHostEdge(req); - const cookieJar = cookies(); + const cookieJar = await cookies(); const edgeConfig = await getAuthEdgeConfig(domain); const returnToQueryParam = getReturnToQueryParam(edgeConfig); - const fern_token = cookieJar.get(COOKIE_FERN_TOKEN)?.value; + const fern_token = fernToken_admin(); const access_token = cookieJar.get("access_token")?.value; const refresh_token = cookieJar.get("refresh_token")?.value; const fernUser = await safeVerifyFernJWTConfig(fern_token, edgeConfig); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/callback/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/callback/route.ts similarity index 96% rename from packages/fern-docs/bundle/src/app/api/fern-docs/auth/callback/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/callback/route.ts index 3686e0925f..0ebb843307 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/callback/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/callback/route.ts @@ -1,13 +1,15 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { getAuthEdgeConfig } from "@fern-docs/edge-config"; +import { HEADER_X_FERN_HOST } from "@fern-docs/utils"; + +import { FernNextResponse } from "@/server/FernNextResponse"; import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; import { getReturnToQueryParam } from "@/server/auth/return-to"; -import { FernNextResponse } from "@/server/FernNextResponse"; import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getAuthEdgeConfig } from "@fern-docs/edge-config"; -import { HEADER_X_FERN_HOST } from "@fern-docs/utils"; -import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; @@ -52,7 +54,7 @@ export async function GET(req: NextRequest): Promise { nextUrl.host = req.headers.get(HEADER_X_FERN_HOST)!; } - if (config?.type === "oauth2" && config.partner === "ory") { + if (config.type === "oauth2" && config.partner === "ory") { nextUrl.pathname = nextUrl.pathname.replace( "/api/fern-docs/auth/callback", "/api/fern-docs/oauth/ory/callback" @@ -61,7 +63,7 @@ export async function GET(req: NextRequest): Promise { destination: nextUrl, allowedDestinations: getAllowedRedirectUrls(config), }); - } else if (config?.type === "sso") { + } else if (config.type === "sso") { nextUrl.pathname = nextUrl.pathname.replace( "/api/fern-docs/auth/callback", "/api/fern-docs/auth/sso/callback" diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/jwt/callback/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/jwt/callback/route.ts similarity index 98% rename from packages/fern-docs/bundle/src/app/api/fern-docs/auth/jwt/callback/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/jwt/callback/route.ts index 07f22974ab..7da32c15a4 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/jwt/callback/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/jwt/callback/route.ts @@ -1,16 +1,18 @@ -import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { getAuthEdgeConfig } from "@fern-docs/edge-config"; +import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; + +import { FernNextResponse } from "@/server/FernNextResponse"; import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; +import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; import { getReturnToQueryParam } from "@/server/auth/return-to"; import { withSecureCookie } from "@/server/auth/with-secure-cookie"; -import { FernNextResponse } from "@/server/FernNextResponse"; import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getAuthEdgeConfig } from "@fern-docs/edge-config"; -import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; @@ -55,7 +57,7 @@ export async function GET(req: NextRequest): Promise { }) : NextResponse.next(); - const cookieJar = cookies(); + const cookieJar = await cookies(); cookieJar.set( COOKIE_FERN_TOKEN, token, diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/logout/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/logout/route.ts similarity index 98% rename from packages/fern-docs/bundle/src/app/api/fern-docs/auth/logout/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/logout/route.ts index 579162959a..5418c04ba4 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/logout/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/logout/route.ts @@ -1,10 +1,6 @@ -import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; -import { getReturnToQueryParam } from "@/server/auth/return-to"; -import { withDeleteCookie } from "@/server/auth/with-secure-cookie"; -import { revokeSessionForToken } from "@/server/auth/workos-session"; -import { FernNextResponse } from "@/server/FernNextResponse"; -import { safeUrl } from "@/server/safeUrl"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getAuthEdgeConfig } from "@fern-docs/edge-config"; import { @@ -12,14 +8,20 @@ import { COOKIE_FERN_TOKEN, COOKIE_REFRESH_TOKEN, } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; + +import { FernNextResponse } from "@/server/FernNextResponse"; +import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; +import { getReturnToQueryParam } from "@/server/auth/return-to"; +import { withDeleteCookie } from "@/server/auth/with-secure-cookie"; +import { revokeSessionForToken } from "@/server/auth/workos-session"; +import { safeUrl } from "@/server/safeUrl"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; export const runtime = "edge"; export async function GET(req: NextRequest): Promise { const domain = getDocsDomainEdge(req); - const cookieJar = cookies(); + const cookieJar = await cookies(); const authConfig = await getAuthEdgeConfig(domain); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/sso/callback/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/sso/callback/route.ts similarity index 98% rename from packages/fern-docs/bundle/src/app/api/fern-docs/auth/sso/callback/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/sso/callback/route.ts index 077731afb1..86fa32c113 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/auth/sso/callback/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/auth/sso/callback/route.ts @@ -1,15 +1,17 @@ +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { getAuthEdgeConfig } from "@fern-docs/edge-config"; +import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; + +import { FernNextResponse } from "@/server/FernNextResponse"; import { getReturnToQueryParam } from "@/server/auth/return-to"; import { withSecureCookie } from "@/server/auth/with-secure-cookie"; import { getWorkOSClientId, workos } from "@/server/auth/workos"; import { encryptSession } from "@/server/auth/workos-session"; -import { FernNextResponse } from "@/server/FernNextResponse"; import { safeUrl } from "@/server/safeUrl"; import { getDocsDomainEdge } from "@/server/xfernhost/edge"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getAuthEdgeConfig } from "@fern-docs/edge-config"; -import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; @@ -102,7 +104,7 @@ export async function GET(req: NextRequest): Promise { // TODO: check if we need to run `getAllowedRedirectUrls(config)` because we don't have the edge config imported here const res = FernNextResponse.redirect(req, { destination: url }); - const cookieJar = cookies(); + const cookieJar = await cookies(); cookieJar.set(COOKIE_FERN_TOKEN, session, withSecureCookie(url.origin)); return res; @@ -120,7 +122,7 @@ export async function GET(req: NextRequest): Promise { function errorResponse() { const errorBody = { error: { - message: "Something went wrong", + message: "Something went wrong!", description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.", }, diff --git a/packages/fern-docs/bundle/src/app/[[...slug]]/changelog.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/changelog/route.ts similarity index 52% rename from packages/fern-docs/bundle/src/app/[[...slug]]/changelog.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/changelog/route.ts index a65bacb8a0..7663255fa1 100644 --- a/packages/fern-docs/bundle/src/app/[[...slug]]/changelog.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/changelog/route.ts @@ -1,32 +1,34 @@ -import { DocsLoader } from "@/server/DocsLoader"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { Feed, Item } from "feed"; +import urlJoin from "url-join"; + import type { DocsV1Read } from "@fern-api/fdr-sdk/client/types"; import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; import { assertNever, withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getFrontmatter } from "@fern-docs/mdx"; -import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { Feed, Item } from "feed"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import urlJoin from "url-join"; +import { COOKIE_FERN_TOKEN, addLeadingSlash } from "@fern-docs/utils"; + +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { FileData } from "@/server/types"; -export const revalidate = 60 * 60 * 24; +const FORMATS = ["rss", "atom", "json"] as const; +type Format = (typeof FORMATS)[number]; -export async function handleChangelog( +export async function GET( req: NextRequest, - { params }: { params: { slug?: string[]; format?: "rss" | "atom" | "json" } } + props: { params: Promise<{ domain: string }> } ): Promise { - const slug = params.slug ?? []; - const path = addLeadingSlash(slug.join("/")); - const format = params.format ?? "rss"; + const { domain } = await props.params; - const domain = getDocsDomainEdge(req); - const host = getHostEdge(req); - const fernToken = cookies().get(COOKIE_FERN_TOKEN)?.value; - const loader = DocsLoader.for(domain, host, fernToken); + const path = addLeadingSlash(req.nextUrl.searchParams.get("slug") ?? ""); + const format = getFormat(req); - const root = await loader.root(); + const fernToken = (await cookies()).get(COOKIE_FERN_TOKEN)?.value; + const loader = await createCachedDocsLoader(domain, fernToken); + const root = await loader.getRoot(); if (!root) { return NextResponse.json(null, { status: 404 }); @@ -50,21 +52,24 @@ export async function handleChangelog( generator: "buildwithfern.com", }); - const pages = await loader.pages(); - const files = await loader.files(); - - node.children.forEach((year) => { - year.children.forEach((month) => { - month.children.forEach((entry) => { - try { - feed.addItem(toFeedItem(entry, domain, pages, files)); - } catch (e) { - console.error(e); - // TODO: sentry - } + const files = await loader.getFiles(); + + await Promise.allSettled( + node.children.flatMap((year) => { + return year.children.flatMap((month) => { + return month.children.map(async (entry) => { + try { + feed.addItem( + await toFeedItem(entry, domain, (id) => loader.getPage(id), files) + ); + } catch (e) { + console.error(e); + // TODO: sentry + } + }); }); - }); - }); + }) + ); if (format === "json") { return new NextResponse(feed.json1(), { @@ -81,20 +86,32 @@ export async function handleChangelog( } } -function toFeedItem( +function isFormat(format: string): format is Format { + return FORMATS.includes(format as Format); +} + +function getFormat(req: NextRequest): Format { + const format = req.nextUrl.searchParams.get("format"); + if (!format || !isFormat(format)) { + return "rss"; + } + return format; +} + +async function toFeedItem( entry: FernNavigation.ChangelogEntryNode, domain: string, - pages: Record, - files: Record -): Item { + getPage: (id: string) => Promise<{ filename: string; markdown: string }>, + files: Record +): Promise { const item: Item = { title: entry.title, link: urlJoin(withDefaultProtocol(domain), entry.slug), date: new Date(entry.date), }; - const markdown = pages[entry.pageId]?.markdown; - if (markdown != null) { + try { + const { markdown } = await getPage(entry.pageId); const { data: frontmatter, content } = getFrontmatter(markdown); item.description = frontmatter.description ?? frontmatter.subtitle ?? frontmatter.excerpt; @@ -102,31 +119,29 @@ function toFeedItem( // TODO: content should be converted into HTML markup item.content = content; - try { - let image: string | undefined; - - // TODO: (rohin) Clean up after safe deploy, but include for back compat - if (frontmatter.image != null && typeof frontmatter.image === "string") { - image = frontmatter.image; - } else if (frontmatter["og:image"] != null) { - image = toUrl(frontmatter["og:image"], files); - } - - if (image != null) { - validateExternalUrl(image); - item.image = { url: image }; - } - } catch (e) { - console.error(e); - // TODO: sentry + let image: string | undefined; + + // TODO: (rohin) Clean up after safe deploy, but include for back compat + if (frontmatter.image != null && typeof frontmatter.image === "string") { + image = frontmatter.image; + } else if (frontmatter["og:image"] != null) { + image = toUrl(frontmatter["og:image"], files); + } + + if (image != null) { + validateExternalUrl(image); + item.image = { url: image }; } + } catch (e) { + console.error(e); + // TODO: sentry } return item; } function toUrl( idOrUrl: DocsV1Read.FileIdOrUrl | undefined, - files: Record + files: Record ): string | undefined { if (idOrUrl == null) { return undefined; @@ -134,7 +149,7 @@ function toUrl( if (idOrUrl.type === "url") { return idOrUrl.value; } else if (idOrUrl.type === "fileId") { - return files[idOrUrl.value]?.url; + return files[idOrUrl.value]?.src; } else { assertNever(idOrUrl); } diff --git a/packages/fern-docs/bundle/src/app/[[...slug]]/llms-full.txt.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms-full.txt/route.ts similarity index 64% rename from packages/fern-docs/bundle/src/app/[[...slug]]/llms-full.txt.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms-full.txt/route.ts index 3d06a9d71e..2b8a8e8438 100644 --- a/packages/fern-docs/bundle/src/app/[[...slug]]/llms-full.txt.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms-full.txt/route.ts @@ -1,31 +1,28 @@ -import { DocsLoader } from "@/server/DocsLoader"; -import { getMarkdownForPath } from "@/server/getMarkdownForPath"; -import { getSectionRoot } from "@/server/getSectionRoot"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { uniqBy } from "es-toolkit/array"; + import { FernNavigation } from "@fern-api/fdr-sdk"; import { CONTINUE, SKIP } from "@fern-api/fdr-sdk/traversers"; import { isNonNullish } from "@fern-api/ui-core-utils"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { uniqBy } from "es-toolkit/array"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; +import { COOKIE_FERN_TOKEN, addLeadingSlash } from "@fern-docs/utils"; -export async function handleLLMSFullTxt( +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { getMarkdownForPath } from "@/server/getMarkdownForPath"; +import { getSectionRoot } from "@/server/getSectionRoot"; + +export async function GET( req: NextRequest, - { params }: { params: { slug?: string[] } } + props: { params: Promise<{ domain: string }> } ): Promise { - const slug = params.slug ?? []; - const path = addLeadingSlash(slug.join("/")); - const domain = getDocsDomainEdge(req); - const host = getHostEdge(req); - const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; - const edgeFlags = await getEdgeFlags(domain); - const loader = DocsLoader.for(domain, host, fern_token).withEdgeFlags( - edgeFlags - ); + const { domain } = await props.params; + + const path = addLeadingSlash(req.nextUrl.searchParams.get("slug") ?? ""); + const fern_token = (await cookies()).get(COOKIE_FERN_TOKEN)?.value; + const loader = await createCachedDocsLoader(domain, fern_token); - const root = getSectionRoot(await loader.root(), path); + const root = getSectionRoot(await loader.getRoot(), path); if (root == null) { return NextResponse.json(null, { status: 404 }); @@ -53,7 +50,7 @@ export async function handleLLMSFullTxt( nodes, (a) => FernNavigation.getPageId(a) ?? a.canonicalSlug ?? a.slug ).map(async (node) => { - const markdown = await getMarkdownForPath(node, loader, edgeFlags); + const markdown = await getMarkdownForPath(node, loader); if (markdown == null) { return undefined; } diff --git a/packages/fern-docs/bundle/src/app/[[...slug]]/llms.txt.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms.txt/route.ts similarity index 84% rename from packages/fern-docs/bundle/src/app/[[...slug]]/llms.txt.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms.txt/route.ts index 355781f8af..320fcf5282 100644 --- a/packages/fern-docs/bundle/src/app/[[...slug]]/llms.txt.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/llms.txt/route.ts @@ -1,15 +1,14 @@ -import { DocsLoader } from "@/server/DocsLoader"; -import { getMarkdownForPath } from "@/server/getMarkdownForPath"; -import { getSectionRoot } from "@/server/getSectionRoot"; -import { getLlmTxtMetadata } from "@/server/llm-txt-md"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; +import { NextRequest, NextResponse } from "next/server"; + import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { CONTINUE, SKIP } from "@fern-api/fdr-sdk/traversers"; import { isNonNullish, withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { COOKIE_FERN_TOKEN, addLeadingSlash } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; +import { addLeadingSlash } from "@fern-docs/utils"; + +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { getMarkdownForPath } from "@/server/getMarkdownForPath"; +import { getSectionRoot } from "@/server/getSectionRoot"; +import { getLlmTxtMetadata } from "@/server/llm-txt-md"; /** * This endpoint follows the https://llmstxt.org/ specification for a LLM-friendly markdown-esque page listing all the pages in the docs. @@ -32,21 +31,16 @@ import { NextRequest, NextResponse } from "next/server"; * - should hidden pages be included under an `## Optional` heading? */ -export async function handleLLMSTxt( +export async function GET( req: NextRequest, - { params }: { params: { slug?: string[] } } + props: { params: Promise<{ domain: string }> } ): Promise { - const path = addLeadingSlash(params.slug?.join("/") ?? ""); - const domain = getDocsDomainEdge(req); - const host = getHostEdge(req); - const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; - const edgeFlags = await getEdgeFlags(domain); - const loader = DocsLoader.for(domain, host, fern_token).withEdgeFlags( - edgeFlags - ); + const { domain } = await props.params; - const root = getSectionRoot(await loader.root(), path); - const pages = await loader.pages(); + const path = addLeadingSlash(req.nextUrl.searchParams.get("slug") ?? ""); + const loader = await createCachedDocsLoader(domain); + + const root = getSectionRoot(await loader.getRoot(), path); if (root == null) { return NextResponse.json(null, { status: 404 }); @@ -71,7 +65,7 @@ export async function handleLLMSTxt( const landingPage = getLandingPage(root); const markdown = landingPage != null - ? await getMarkdownForPath(landingPage, loader, edgeFlags) + ? await getMarkdownForPath(landingPage, loader) : undefined; // traverse the tree in a depth-first manner to collect all the nodes that have markdown content @@ -125,17 +119,17 @@ export async function handleLLMSTxt( return CONTINUE; }); - const docs = pageInfos - .map( - ( + const docs = await Promise.allSettled( + pageInfos.map( + async ( pageInfo - ): { + ): Promise<{ title: string; description: string | undefined; href: string; - } => { + }> => { if (pageInfo.pageId != null) { - const page = pages[pageInfo.pageId]; + const page = await loader.getPage(pageInfo.pageId); if (page != null) { const { title, description } = getLlmTxtMetadata( page.markdown, @@ -166,10 +160,7 @@ export async function handleLLMSTxt( }; } ) - .map( - (doc) => - `- [${doc.title}](${doc.href})${doc.description != null ? `: ${doc.description}` : ""}` - ); + ); const endpoints = endpointPageInfos .map((endpointPageInfo) => { @@ -194,7 +185,16 @@ export async function handleLLMSTxt( [ // if there's a landing page, use the llm-friendly markdown version instead of the ${root.title} markdown?.content ?? `# ${root.title}`, - docs.length > 0 ? `## Docs\n\n${docs.join("\n")}` : undefined, + docs.length > 0 + ? `## Docs\n\n${docs + .filter((doc) => doc.status === "fulfilled") + .map((doc) => doc.value) + .map( + (doc) => + `- [${doc.title}](${doc.href})${doc.description != null ? `: ${doc.description}` : ""}` + ) + .join("\n")}` + : undefined, endpoints.length > 0 ? `## API Docs\n\n${endpoints.join("\n")}` : undefined, diff --git a/packages/fern-docs/bundle/src/app/[[...slug]]/markdown.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/markdown/route.ts similarity index 54% rename from packages/fern-docs/bundle/src/app/[[...slug]]/markdown.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/markdown/route.ts index 39125e63ed..bc130864c5 100644 --- a/packages/fern-docs/bundle/src/app/[[...slug]]/markdown.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/markdown/route.ts @@ -1,46 +1,42 @@ -import { DocsLoader } from "@/server/DocsLoader"; +import { notFound } from "next/navigation"; +import { NextRequest, NextResponse } from "next/server"; + +import { addLeadingSlash } from "@fern-docs/utils"; + +import { createCachedDocsLoader } from "@/server/docs-loader"; import { getMarkdownForPath, getPageNodeForPath, } from "@/server/getMarkdownForPath"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { addLeadingSlash, COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { notFound } from "next/navigation"; -import { NextRequest, NextResponse } from "next/server"; /** * This endpoint returns the markdown content of any page in the docs by adding `.md` or `.mdx` to the end of any docs page. */ -export async function handleMarkdown( +export async function GET( req: NextRequest, - { params }: { params: { slug?: string[] } } + props: { params: Promise<{ domain: string }> } ): Promise { - const slug = params.slug ?? []; - const path = addLeadingSlash(slug.join("/")); - const domain = getDocsDomainEdge(req); - const host = getHostEdge(req); - const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; - const edgeFlags = await getEdgeFlags(domain); - const loader = DocsLoader.for(domain, host, fern_token).withEdgeFlags( - edgeFlags - ); + const { domain } = await props.params; + + const path = addLeadingSlash(req.nextUrl.searchParams.get("slug") ?? ""); + const loader = await createCachedDocsLoader(domain); + + const node = getPageNodeForPath(await loader.getRoot(), path); - const node = getPageNodeForPath(await loader.root(), path); - console.log(path, node); if (node == null) { + console.error(`[${domain}] Node not found: ${path}`); notFound(); } // If the page is authed, but the user is not authed, return a 403 - if (node.authed && !(await loader.isAuthed())) { + if (node.authed) { return new NextResponse(null, { status: 403 }); } - const markdown = await getMarkdownForPath(node, loader, edgeFlags); + const markdown = await getMarkdownForPath(node, loader); if (markdown == null) { + console.error(`[${domain}] Markdown not found: ${path}`); notFound(); } diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/oauth/ory/callback/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/ory/callback/route.ts similarity index 99% rename from packages/fern-docs/bundle/src/app/api/fern-docs/oauth/ory/callback/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/ory/callback/route.ts index 5b0b8b1d50..997e5a1d24 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/oauth/ory/callback/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/ory/callback/route.ts @@ -1,12 +1,6 @@ -import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; -import { signFernJWT } from "@/server/auth/FernJWT"; -import { OryOAuth2Client } from "@/server/auth/ory"; -import { getReturnToQueryParam } from "@/server/auth/return-to"; -import { withSecureCookie } from "@/server/auth/with-secure-cookie"; -import { FernNextResponse } from "@/server/FernNextResponse"; -import { redirectWithLoginError } from "@/server/redirectWithLoginError"; -import { safeUrl } from "@/server/safeUrl"; -import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { FernUser, OryAccessTokenSchema } from "@fern-docs/auth"; import { getAuthEdgeConfig } from "@fern-docs/edge-config"; @@ -15,8 +9,16 @@ import { COOKIE_FERN_TOKEN, COOKIE_REFRESH_TOKEN, } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; + +import { FernNextResponse } from "@/server/FernNextResponse"; +import { signFernJWT } from "@/server/auth/FernJWT"; +import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; +import { OryOAuth2Client } from "@/server/auth/ory"; +import { getReturnToQueryParam } from "@/server/auth/return-to"; +import { withSecureCookie } from "@/server/auth/with-secure-cookie"; +import { redirectWithLoginError } from "@/server/redirectWithLoginError"; +import { safeUrl } from "@/server/safeUrl"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; export const runtime = "edge"; @@ -24,7 +26,7 @@ export async function GET(req: NextRequest): Promise { const domain = getDocsDomainEdge(req); const host = getHostEdge(req); const config = await getAuthEdgeConfig(domain); - const cookieJar = cookies(); + const cookieJar = await cookies(); const code = req.nextUrl.searchParams.get("code"); const return_to = req.nextUrl.searchParams.get(getReturnToQueryParam(config)); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/oauth/webflow/callback/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/webflow/callback/route.ts similarity index 98% rename from packages/fern-docs/bundle/src/app/api/fern-docs/oauth/webflow/callback/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/webflow/callback/route.ts index c676a8f51d..12c0664b9f 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/oauth/webflow/callback/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/oauth/webflow/callback/route.ts @@ -1,15 +1,18 @@ +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +import { WebflowClient } from "webflow-api"; + +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { getAuthEdgeConfig } from "@fern-docs/edge-config"; + +import { FernNextResponse } from "@/server/FernNextResponse"; import { getAllowedRedirectUrls } from "@/server/auth/allowed-redirects"; import { getReturnToQueryParam } from "@/server/auth/return-to"; import { withSecureCookie } from "@/server/auth/with-secure-cookie"; -import { FernNextResponse } from "@/server/FernNextResponse"; import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getAuthEdgeConfig } from "@fern-docs/edge-config"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import { WebflowClient } from "webflow-api"; export const runtime = "edge"; @@ -73,7 +76,7 @@ export async function GET(req: NextRequest): Promise { allowedDestinations: getAllowedRedirectUrls(config), }) : NextResponse.next(); - cookies().set( + (await cookies()).set( "access_token", accessToken, withSecureCookie(withDefaultProtocol(host)) diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/org/org-for-url/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/org/org-for-url/route.ts similarity index 79% rename from packages/fern-docs/bundle/src/app/api/fern-docs/org/org-for-url/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/org/org-for-url/route.ts index c812dcc8ca..ebeabc6bd0 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/org/org-for-url/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/org/org-for-url/route.ts @@ -1,7 +1,8 @@ -import { getOrgMetadataForDomain } from "@/server/auth/metadata-for-url"; -import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { NextRequest, NextResponse } from "next/server"; +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; + export const runtime = "edge"; export interface DocsMetadata { @@ -16,7 +17,8 @@ export async function GET(req: NextRequest): Promise { return NextResponse.json({ error: "Invalid domain" }, { status: 400 }); } - const metadata = await getOrgMetadataForDomain(domain); + const loader = await createCachedDocsLoader(domain); + const metadata = await loader.getMetadata(); if (metadata) { return NextResponse.json(metadata, { status: 200 }); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/preview/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/preview/route.ts similarity index 89% rename from packages/fern-docs/bundle/src/app/api/fern-docs/preview/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/preview/route.ts index d74f36e7dc..292af5cc62 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/preview/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/preview/route.ts @@ -1,25 +1,31 @@ +import { cookies } from "next/headers"; +import { notFound } from "next/navigation"; +import { NextRequest, NextResponse } from "next/server"; + +import { getEnv } from "@vercel/functions"; + +import { COOKIE_FERN_DOCS_PREVIEW } from "@fern-docs/utils"; + import { withDeleteCookie, withSecureCookie, } from "@/server/auth/with-secure-cookie"; import { redirectResponse } from "@/server/serverResponse"; -import { COOKIE_FERN_DOCS_PREVIEW } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { notFound } from "next/navigation"; -import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; export async function GET(req: NextRequest): Promise { + const { VERCEL_ENV } = getEnv(); + // Only allow preview in dev and preview deployments - if (process.env.VERCEL_ENV === "production") { + if (VERCEL_ENV === "production") { return notFound(); } const host = req.nextUrl.searchParams.get("host"); const site = req.nextUrl.searchParams.get("site"); const clear = req.nextUrl.searchParams.get("clear"); - const cookieStore = cookies(); + const cookieStore = await cookies(); if (typeof host === "string") { cookieStore.set( COOKIE_FERN_DOCS_PREVIEW, diff --git a/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/revalidate/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/revalidate/route.ts new file mode 100644 index 0000000000..3e12564176 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/revalidate/route.ts @@ -0,0 +1,43 @@ +import { revalidateTag } from "next/cache"; +import { NextRequest, NextResponse } from "next/server"; + +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { + HEADER_X_FERN_HOST, + addLeadingSlash, + conformTrailingSlash, +} from "@fern-docs/utils"; + +import { createCachedDocsLoader } from "@/server/docs-loader"; + +export async function GET( + req: NextRequest, + props: { params: Promise<{ domain: string }> } +): Promise { + const { domain } = await props.params; + + revalidateTag(domain); + + if (req.nextUrl.searchParams.get("regenerate") === "true") { + const docs = await createCachedDocsLoader(domain); + const root = await docs.unsafe_getFullRoot(); + const collector = FernNavigation.NodeCollector.collect(root); + + const promises = collector.slugs.map((slug) => { + return fetch( + `${req.nextUrl.origin}${conformTrailingSlash(addLeadingSlash(slug))}`, + { + method: "HEAD", + cache: "no-store", + headers: { [HEADER_X_FERN_HOST]: domain }, + } + ); + }); + + await Promise.all(promises); + } + + return NextResponse.json({ + message: `Revalidated ${domain}`, + }); +} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/chat/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/chat/route.ts similarity index 65% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/chat/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/chat/route.ts index 23a53007ef..907f333e08 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/chat/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/chat/route.ts @@ -1,10 +1,12 @@ -import { track } from "@/server/analytics/posthog"; -import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; -import { getOrgMetadataForDomain } from "@/server/auth/metadata-for-url"; -import { openaiApiKey, turbopufferApiKey } from "@/server/env-variables"; -import { getDocsDomainEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"; import { createOpenAI } from "@ai-sdk/openai"; +import { EmbeddingModel, embed, streamText, tool } from "ai"; +import { initLogger, traced, wrapAISDKModel } from "braintrust"; +import { z } from "zod"; + import { getAuthEdgeConfig, getEdgeFlags } from "@fern-docs/edge-config"; import { createDefaultSystemPrompt } from "@fern-docs/search-server"; import { @@ -12,11 +14,12 @@ import { toDocuments, } from "@fern-docs/search-server/turbopuffer"; import { COOKIE_FERN_TOKEN, withoutStaging } from "@fern-docs/utils"; -import { embed, EmbeddingModel, streamText, tool } from "ai"; -import { initLogger, wrapAISDKModel } from "braintrust"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; + +import { track } from "@/server/analytics/posthog"; +import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { openaiApiKey, turbopufferApiKey } from "@/server/env-variables"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; export const maxDuration = 60; export const revalidate = 0; @@ -43,12 +46,13 @@ export async function POST(req: NextRequest) { const namespace = `${withoutStaging(domain)}_${embeddingModel.modelId}`; const { messages } = await req.json(); - const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain)); - if (orgMetadata == null) { + const loader = await createCachedDocsLoader(domain); + const metadata = await loader.getMetadata(); + if (metadata == null) { return NextResponse.json("Not found", { status: 404 }); } - if (orgMetadata.isPreviewUrl) { + if (metadata.isPreview) { return NextResponse.json({ added: 0, updated: 0, @@ -67,7 +71,7 @@ export async function POST(req: NextRequest) { throw new Error(`Ask AI is not enabled for ${domain}`); } - const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; + const fern_token = (await cookies()).get(COOKIE_FERN_TOKEN)?.value; const user = await safeVerifyFernJWTConfig(fern_token, authEdgeConfig); const lastUserMessage: string | undefined = messages.findLast( @@ -86,51 +90,54 @@ export async function POST(req: NextRequest) { date: new Date().toDateString(), documents, }); - const result = streamText({ - model: languageModel, - system, - messages, - maxSteps: 10, - maxRetries: 3, - tools: { - search: tool({ - description: - "Search the knowledge base for the user's query. Semantic search is enabled.", - parameters: z.object({ - query: z.string(), + // eslint-disable-next-line @typescript-eslint/await-thenable + const result = await traced(() => + streamText({ + model: languageModel, + system, + messages, + maxSteps: 10, + maxRetries: 3, + tools: { + search: tool({ + description: + "Search the knowledge base for the user's query. Semantic search is enabled.", + parameters: z.object({ + query: z.string(), + }), + async execute({ query }) { + const response = await runQueryTurbopuffer(query, { + embeddingModel, + namespace, + authed: user != null, + roles: user?.roles ?? [], + }); + return response.map((hit) => { + const { domain, pathname, hash } = hit.attributes; + const url = `https://${domain}${pathname}${hash ?? ""}`; + return { url, ...hit.attributes }; + }); + }, }), - async execute({ query }) { - const response = await runQueryTurbopuffer(query, { - embeddingModel, - namespace, - authed: user != null, - roles: user?.roles ?? [], - }); - return response.map((hit) => { - const { domain, pathname, hash } = hit.attributes; - const url = `https://${domain}${pathname}${hash ?? ""}`; - return { url, ...hit.attributes }; - }); - }, - }), - }, - onFinish: async (e) => { - const end = Date.now(); - await track("ask_ai", { - languageModel: languageModel.modelId, - embeddingModel: embeddingModel.modelId, - durationMs: end - start, - domain, - namespace, - numToolCalls: e.toolCalls.length, - finishReason: e.finishReason, - ...e.usage, - }); - e.warnings?.forEach((warning) => { - console.warn(warning); - }); - }, - }); + }, + onFinish: async (e) => { + const end = Date.now(); + await track("ask_ai", { + languageModel: languageModel.modelId, + embeddingModel: embeddingModel.modelId, + durationMs: end - start, + domain, + namespace, + numToolCalls: e.toolCalls.length, + finishReason: e.finishReason, + ...e.usage, + }); + e.warnings?.forEach((warning) => { + console.warn(warning); + }); + }, + }) + ); const response = result.toDataStreamResponse(); response.headers.set("Access-Control-Allow-Origin", "*"); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/facet/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/facet/route.ts similarity index 95% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/facet/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/facet/route.ts index 88f6abb84c..78a1dcf9ce 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/facet/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/facet/route.ts @@ -1,12 +1,14 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { algoliasearch } from "algoliasearch"; + +import { fetchFacetValues } from "@fern-docs/search-server/algolia"; + import { algoliaAppId } from "@/server/env-variables"; import { selectFirst } from "@/server/utils/selectFirst"; import { toArray } from "@/server/utils/toArray"; -import { fetchFacetValues } from "@fern-docs/search-server/algolia"; -import { algoliasearch } from "algoliasearch"; -import { NextRequest, NextResponse } from "next/server"; export const maxDuration = 10; -export const dynamic = "force-dynamic"; export async function GET(req: NextRequest): Promise { const filters = toArray(req.nextUrl.searchParams.getAll("filters")); diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/key/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/key/route.ts similarity index 81% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/key/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/key/route.ts index d9ab86d7f2..0d40144152 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/key/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/key/route.ts @@ -1,8 +1,6 @@ -import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; -import { getOrgMetadataForDomain } from "@/server/auth/metadata-for-url"; -import { algoliaAppId, algoliaSearchApikey } from "@/server/env-variables"; -import { selectFirst } from "@/server/utils/selectFirst"; -import { getDocsDomainEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + import { getAuthEdgeConfig } from "@fern-docs/edge-config"; import { DEFAULT_SEARCH_API_KEY_EXPIRATION_SECONDS, @@ -10,28 +8,27 @@ import { getSearchApiKey, } from "@fern-docs/search-server/algolia/edge"; import { COOKIE_FERN_TOKEN, withoutStaging } from "@fern-docs/utils"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; + +import { safeVerifyFernJWTConfig } from "@/server/auth/FernJWT"; +import { algoliaAppId, algoliaSearchApikey } from "@/server/env-variables"; +import { getDocsUrlMetadata } from "@/server/getDocsUrlMetadata"; +import { selectFirst } from "@/server/utils/selectFirst"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; export const runtime = "edge"; export const maxDuration = 10; -export const dynamic = "force-dynamic"; export async function GET(req: NextRequest): Promise { const domain = getDocsDomainEdge(req); - const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain)); - if (orgMetadata == null) { - return NextResponse.json("Not found", { status: 404 }); - } - - if (orgMetadata.isPreviewUrl) { + const metadata = await getDocsUrlMetadata(domain); + if (metadata.isPreview) { return NextResponse.json("Search is not supported for preview URLs", { status: 400, }); } - const fern_token = cookies().get(COOKIE_FERN_TOKEN)?.value; + const fern_token = (await cookies()).get(COOKIE_FERN_TOKEN)?.value; const user = await safeVerifyFernJWTConfig( fern_token, await getAuthEdgeConfig(domain) diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/algolia/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/algolia/route.ts similarity index 90% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/algolia/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/algolia/route.ts index d8d28a0d74..9855ae4f79 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/algolia/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/algolia/route.ts @@ -1,36 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getAuthEdgeConfig, getEdgeFlags } from "@fern-docs/edge-config"; +import { + SEARCH_INDEX, + algoliaIndexSettingsTask, + algoliaIndexerTask, +} from "@fern-docs/search-server/algolia"; +import { addLeadingSlash, withoutStaging } from "@fern-docs/utils"; + import { track } from "@/server/analytics/posthog"; -import { getOrgMetadataForDomain } from "@/server/auth/metadata-for-url"; +import { createCachedDocsLoader } from "@/server/docs-loader"; import { algoliaAppId, algoliaWriteApiKey, fdrEnvironment, - fernToken, + fernToken_admin, } from "@/server/env-variables"; import { Gate, withBasicTokenAnonymous } from "@/server/withRbac"; import { getDocsDomainEdge } from "@/server/xfernhost/edge"; -import { getAuthEdgeConfig, getEdgeFlags } from "@fern-docs/edge-config"; -import { - SEARCH_INDEX, - algoliaIndexSettingsTask, - algoliaIndexerTask, -} from "@fern-docs/search-server/algolia"; -import { addLeadingSlash, withoutStaging } from "@fern-docs/utils"; -import { NextRequest, NextResponse } from "next/server"; export const maxDuration = 800; // 13 minutes -export const dynamic = "force-dynamic"; export async function GET(req: NextRequest): Promise { const domain = getDocsDomainEdge(req); try { - const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain)); - if (orgMetadata == null) { + const loader = await createCachedDocsLoader(domain); + const metadata = await loader.getMetadata(); + if (metadata == null) { return NextResponse.json("Not found", { status: 404 }); } // If the domain is a preview URL, we don't want to reindex - if (orgMetadata.isPreviewUrl) { + if (metadata.isPreview) { return NextResponse.json({ added: 0, updated: 0, @@ -56,7 +58,7 @@ export async function GET(req: NextRequest): Promise { writeApiKey: algoliaWriteApiKey(), indexName: SEARCH_INDEX, environment: fdrEnvironment(), - fernToken: fernToken(), + fernToken: fernToken_admin(), domain: withoutStaging(domain), authed: (node) => { if (authEdgeConfig == null) { diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/turbopuffer/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/turbopuffer/route.ts similarity index 90% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/turbopuffer/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/turbopuffer/route.ts index bee20bb355..1cba1be1a2 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/reindex/turbopuffer/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/reindex/turbopuffer/route.ts @@ -1,15 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; + import { createOpenAI } from "@ai-sdk/openai"; +import { embedMany } from "ai"; + import { getAuthEdgeConfig, getEdgeFlags } from "@fern-docs/edge-config"; import { turbopufferUpsertTask } from "@fern-docs/search-server/turbopuffer"; import { addLeadingSlash, withoutStaging } from "@fern-docs/utils"; -import { embedMany } from "ai"; -import { NextRequest, NextResponse } from "next/server"; import { track } from "@/server/analytics/posthog"; -import { getOrgMetadataForDomain } from "@/server/auth/metadata-for-url"; +import { createCachedDocsLoader } from "@/server/docs-loader"; import { fdrEnvironment, - fernToken, + fernToken_admin, openaiApiKey, turbopufferApiKey, } from "@/server/env-variables"; @@ -17,7 +19,6 @@ import { Gate, withBasicTokenAnonymous } from "@/server/withRbac"; import { getDocsDomainEdge } from "@/server/xfernhost/edge"; export const maxDuration = 800; // 13 minutes -export const dynamic = "force-dynamic"; export async function GET(req: NextRequest): Promise { const openai = createOpenAI({ apiKey: openaiApiKey() }); @@ -29,13 +30,14 @@ export async function GET(req: NextRequest): Promise { const namespace = `${withoutStaging(domain)}_${embeddingModel.modelId}`; try { - const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain)); - if (orgMetadata == null) { + const loader = await createCachedDocsLoader(domain); + const metadata = await loader.getMetadata(); + if (metadata == null) { return NextResponse.json("Not found", { status: 404 }); } // If the domain is a preview URL, we don't want to reindex - if (orgMetadata.isPreviewUrl) { + if (metadata.isPreview) { return NextResponse.json( { added: 0, @@ -62,7 +64,7 @@ export async function GET(req: NextRequest): Promise { namespace, payload: { environment: fdrEnvironment(), - fernToken: fernToken(), + fernToken: fernToken_admin(), domain: withoutStaging(domain), ...edgeFlags, }, diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/suggest/route.ts b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/suggest/route.ts similarity index 95% rename from packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/suggest/route.ts rename to packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/suggest/route.ts index 960916124f..2beb0b629b 100644 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v2/suggest/route.ts +++ b/packages/fern-docs/bundle/src/app/[domain]/api/fern-docs/search/v2/suggest/route.ts @@ -1,22 +1,26 @@ -import { track } from "@/server/analytics/posthog"; -import { algoliaAppId, anthropicApiKey } from "@/server/env-variables"; -import { getDocsDomainEdge } from "@/server/xfernhost/edge"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + import { createAnthropic } from "@ai-sdk/anthropic"; import { searchClient } from "@algolia/client-search"; +import { getEnv } from "@vercel/functions"; +import { kv } from "@vercel/kv"; +import { streamObject } from "ai"; +import { z } from "zod"; + import { getEdgeFlags } from "@fern-docs/edge-config"; import { SuggestionsSchema } from "@fern-docs/search-server"; import { - SEARCH_INDEX, type AlgoliaRecord, + SEARCH_INDEX, } from "@fern-docs/search-server/algolia"; import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; -import { kv } from "@vercel/kv"; -import { streamObject } from "ai"; -import { cookies } from "next/headers"; -import { NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -const DEPLOYMENT_ID = process.env.VERCEL_DEPLOYMENT_ID ?? "development"; +import { track } from "@/server/analytics/posthog"; +import { algoliaAppId, anthropicApiKey } from "@/server/env-variables"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; + +const DEPLOYMENT_ID = getEnv().VERCEL_DEPLOYMENT_ID ?? "development"; const PREFIX = `docs:${DEPLOYMENT_ID}`; // Allow streaming responses up to 30 seconds @@ -33,7 +37,7 @@ export async function POST(req: NextRequest): Promise { const start = Date.now(); const domain = getDocsDomainEdge(req); const edgeFlags = await getEdgeFlags(domain); - const cookieJar = cookies(); + const cookieJar = await cookies(); if (!edgeFlags.isAskAiEnabled) { throw new Error(`Ask AI is not enabled for ${domain}`); diff --git a/packages/fern-docs/bundle/src/app/[domain]/bottom-nav.tsx b/packages/fern-docs/bundle/src/app/[domain]/bottom-nav.tsx new file mode 100644 index 0000000000..0163440e19 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/bottom-nav.tsx @@ -0,0 +1,91 @@ +import "server-only"; + +import React from "react"; + +import { Separator } from "@radix-ui/react-separator"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +import { MaybeFernLink } from "@/components/components/FernLink"; +import { MdxServerComponent } from "@/components/mdx/server-component"; +import { MdxSerializer } from "@/server/mdx-serializer"; + +export function BottomNavigation({ + neighbors, + serialize, +}: { + serialize: MdxSerializer; + neighbors: { + prev?: { + title: string; + href: string; + excerpt?: string; + }; + next?: { + title: string; + href: string; + excerpt?: string; + }; + }; +}) { + if (neighbors.prev == null && neighbors.next == null) { + return ( + + ); + } + + return ( + + ); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/dynamic/@headertabs/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/dynamic/@headertabs/[slug]/page.tsx new file mode 100644 index 0000000000..264fb273d6 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/dynamic/@headertabs/[slug]/page.tsx @@ -0,0 +1,32 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import { getFernToken } from "@/app/fern-token"; +import { HeaderTabsList } from "@/components/header/HeaderTabsList"; +import { createCachedDocsLoader } from "@/server/docs-loader"; + +export default async function HeaderTabsPage({ + params, +}: { + params: Promise<{ domain: string; slug: string }>; +}) { + const { domain, slug } = await params; + const loader = await createCachedDocsLoader(domain, await getFernToken()); + const rootPromise = loader.getRoot(); + const layout = await loader.getLayout(); + + if (layout.tabsPlacement !== "HEADER") { + return null; + } + + const findNode = FernNavigation.utils.findNode( + await rootPromise, + slugjoin(slug) + ); + + if (findNode.type !== "found") { + return null; + } + + return ; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/dynamic/@sidebar/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/dynamic/@sidebar/[slug]/page.tsx new file mode 100644 index 0000000000..4509b94ec5 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/dynamic/@sidebar/[slug]/page.tsx @@ -0,0 +1,35 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import { getFernToken } from "@/app/fern-token"; +import { SidebarTabsList } from "@/components/sidebar/SidebarTabsList"; +import { SidebarTabsRoot } from "@/components/sidebar/SidebarTabsRoot"; +import { SidebarRootNode } from "@/components/sidebar/nodes/SidebarRootNode"; +import { createCachedDocsLoader } from "@/server/docs-loader"; + +export default async function SidebarPage({ + params, +}: { + params: Promise<{ domain: string; slug: string }>; +}) { + const { domain, slug } = await params; + const loader = await createCachedDocsLoader(domain, await getFernToken()); + const [root, layout] = await Promise.all([ + loader.getRoot(), + loader.getLayout(), + ]); + const findNode = FernNavigation.utils.findNode(root, slugjoin(slug)); + if (findNode.type !== "found") { + return null; + } + return ( + <> + {findNode.tabs && findNode.tabs.length > 0 && ( + + + + )} + {findNode.sidebar && } + + ); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/loading.tsx b/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/loading.tsx new file mode 100644 index 0000000000..2b92e7bb4d --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingDocs from "@/components/shared-loading"; + +export default function Loading() { + return ; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/page.tsx new file mode 100644 index 0000000000..2bd4e86ee1 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/dynamic/[slug]/page.tsx @@ -0,0 +1,34 @@ +import "server-only"; + +import { Metadata } from "next/types"; + +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import { getFernToken } from "@/app/fern-token"; +import SharedPage, { + generateMetadata as _generateMetadata, +} from "@/components/shared-page"; + +export default async function DynamicPage(props: { + params: Promise<{ slug: string; domain: string }>; +}) { + const { domain, slug } = await props.params; + return ( + + ); +} + +export async function generateMetadata(props: { + params: Promise<{ slug: string; domain: string }>; +}): Promise { + const { domain, slug } = await props.params; + return _generateMetadata({ + domain, + slug: slugjoin(slug), + fernToken: await getFernToken(), + }); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/dynamic/layout.tsx b/packages/fern-docs/bundle/src/app/[domain]/dynamic/layout.tsx new file mode 100644 index 0000000000..41c134596e --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/dynamic/layout.tsx @@ -0,0 +1,27 @@ +import { getFernToken } from "@/app/fern-token"; +import SharedLayout from "@/components/shared-layout"; + +export default async function Layout({ + children, + params, + headertabs, + sidebar, +}: { + children: React.ReactNode; + params: Promise<{ domain: string }>; + headertabs: React.ReactNode; + sidebar: React.ReactNode; +}) { + const { domain } = await params; + const fernToken = await getFernToken(); + return ( + + {children} + + ); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/loading.tsx b/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/loading.tsx new file mode 100644 index 0000000000..1e3d8ed71c --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import { PlaygroundEndpointSkeleton } from "@/components/playground/endpoint/PlaygroundEndpointSkeleton"; + +export default function Loading() { + return ; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/page.tsx new file mode 100644 index 0000000000..d9134befa4 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/explorer/[slug]/page.tsx @@ -0,0 +1,74 @@ +import { cookies, headers } from "next/headers"; +import { notFound, redirect } from "next/navigation"; + +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { + createEndpointContext, + createWebSocketContext, +} from "@fern-api/fdr-sdk/api-definition"; +import { COOKIE_FERN_TOKEN, conformTrailingSlash } from "@fern-docs/utils"; + +import { PlaygroundEndpoint } from "@/components/playground/endpoint/PlaygroundEndpoint"; +import { conformExplorerRoute } from "@/components/playground/utils/explorer-route"; +import { PlaygroundWebSocket } from "@/components/playground/websocket/PlaygroundWebSocket"; +import { createCachedDocsLoader } from "@/server/docs-loader"; + +export default async function Page(props: { + params: Promise<{ slug: string; domain: string }>; +}) { + const [{ domain, slug: slugProp }, cookieJar, headersList] = + await Promise.all([props.params, cookies(), headers()]); + console.debug(`[${domain}] Loading API Explorer page`); + + const slug = FernNavigation.slugjoin(headersList.get("x-basepath"), slugProp); + const fern_token = cookieJar.get(COOKIE_FERN_TOKEN)?.value; + const loader = await createCachedDocsLoader(domain, fern_token); + + console.debug(`[${loader.domain}] Loading API Explorer for slug: ${slug}`); + const root = await loader.getRoot(); + + const found = FernNavigation.utils.findNode(root, slug); + if (found.type !== "found") { + if (found.redirect) { + console.log( + `[${loader.domain}] Redirecting from ${slug} to ${found.redirect}` + ); + // follows the route path hierarchy + redirect(conformTrailingSlash(conformExplorerRoute(found.redirect))); + } + + console.error(`[${loader.domain}] Could not find node for slug: ${slug}`); + notFound(); + } + const node = found.node; + if (!FernNavigation.isApiLeaf(node)) { + console.error(`[${loader.domain}] Found non-leaf node for slug: ${slug}`); + notFound(); + } + const api = await loader.getApi(node.apiDefinitionId); + + if (node.type === "endpoint") { + const context = createEndpointContext(node, api); + if (context == null) { + console.error( + `[${loader.domain}] Could not create endpoint context for slug: ${slug}` + ); + notFound(); + } + return ; + } else if (node.type === "webSocket") { + const context = createWebSocketContext(node, api); + if (context == null) { + console.error( + `[${loader.domain}] Could not create web socket context for slug: ${slug}` + ); + notFound(); + } + return ; + } + console.error( + `[${loader.domain}] Found non-visitable node for slug: ${slug}`, + node + ); + notFound(); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/explorer/layout.tsx b/packages/fern-docs/bundle/src/app/[domain]/explorer/layout.tsx new file mode 100644 index 0000000000..247a031207 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/explorer/layout.tsx @@ -0,0 +1,51 @@ +import { getFernToken } from "@/app/fern-token"; +import { HorizontalSplitPane } from "@/components/playground/VerticalSplitPane"; +import { PlaygroundEndpointSelectorContent } from "@/components/playground/endpoint/PlaygroundEndpointSelectorContent"; +import { flattenApiSection } from "@/components/playground/utils/flatten-apis"; +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { ApiExplorerFlags } from "@/state/api-explorer-flags"; + +export default async function Layout({ + params, + children, +}: { + children: React.ReactNode; + params: Promise<{ domain: string }>; +}) { + const { domain } = await params; + + console.debug(`[${domain}] Loading API Explorer layout`); + const loader = await createCachedDocsLoader(domain, await getFernToken()); + const [root, edgeFlags] = await Promise.all([ + loader.getRoot(), + loader.getEdgeFlags(), + ]); + const apiGroups = flattenApiSection(root); + + return ( +
+ + + + {children} + +
+ ); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/layout.tsx b/packages/fern-docs/bundle/src/app/[domain]/layout.tsx new file mode 100644 index 0000000000..66384a00d3 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/layout.tsx @@ -0,0 +1,272 @@ +import { Metadata, Viewport } from "next/types"; +import React from "react"; +import { preload } from "react-dom"; + +import { getEnv } from "@vercel/functions"; +import { compact } from "es-toolkit/array"; +import tinycolor from "tinycolor2"; + +import { DocsV1Read, DocsV2Read } from "@fern-api/fdr-sdk/client/types"; +import { isNonNullish } from "@fern-api/ui-core-utils"; +import { + getCustomerAnalytics as deprecated_getCustomerAnalytics, + getEdgeFlags, + getLaunchDarklySettings, + getSeoDisabled, +} from "@fern-docs/edge-config"; + +import { CustomerAnalytics } from "@/components/analytics/CustomerAnalytics"; +import { BgImageGradient } from "@/components/components/BgImageGradient"; +import { JavascriptProvider } from "@/components/components/JavascriptProvider"; +import { withJsConfig } from "@/components/components/with-js-config"; +import { FeatureFlagProvider } from "@/components/feature-flags/FeatureFlagProvider"; +import SearchV2 from "@/components/search"; +import { renderThemeStylesheet } from "@/components/themes/stylesheet/renderThemeStylesheet"; +import { DocsLoader, createCachedDocsLoader } from "@/server/docs-loader"; +import type { RgbaColor } from "@/server/types"; +import { DarkCode } from "@/state/dark-code"; +import { Domain } from "@/state/domain"; +import { LaunchDarklyInfo } from "@/state/feature-flags"; +import { DefaultLanguage } from "@/state/language"; + +import { GlobalStyles } from "../global-styles"; +import { toImageDescriptor } from "../seo"; +import { ThemeProvider } from "../theme"; + +export default async function Layout(props: { + children: React.ReactNode; + params: Promise<{ domain: string }>; +}) { + const params = await props.params; + + const { children } = props; + + const domain = params.domain; + const loader = await createCachedDocsLoader(domain); + const [ + config, + edgeFlags, + files, + colors, + layout, + deprecated_customerAnalytics, + launchDarkly, + ] = await Promise.all([ + loader.getConfig(), + getEdgeFlags(domain), + loader.getFiles(), + loader.getColors(), + loader.getLayout(), + deprecated_getCustomerAnalytics(domain), + getLaunchDarklyInfo(loader), + ]); + + generatePreloadHrefs(config.typographyV2, files); + const stylesheet = renderThemeStylesheet({ + colorsConfig: colors, + typography: config.typographyV2, + layoutConfig: config.layout, + css: config.css, + files, + hasTabs: true, // todo: fix this + }); + + const { VERCEL_ENV } = getEnv(); + + const jsConfig = withJsConfig(config.js, files); + + return ( + + + {config.defaultLanguage != null && ( + + )} + + {/* */} + + + {stylesheet} + + + {children} + + + + + {jsConfig != null && } + {VERCEL_ENV === "production" && ( + + )} + + ); +} + +// TODO: delete this once we've migrated all customers to the new analytics config +function mergeCustomerAnalytics( + deprecated_customerAnalytics: Awaited< + ReturnType + >, + config: DocsV1Read.AnalyticsConfig | undefined +): Partial { + return { + ga4: deprecated_customerAnalytics?.ga4?.measurementId + ? { measurementId: deprecated_customerAnalytics.ga4.measurementId } + : undefined, + gtm: deprecated_customerAnalytics?.gtm?.tagId + ? { containerId: deprecated_customerAnalytics.gtm.tagId } + : undefined, + ...config, + }; +} + +async function getLaunchDarklyInfo( + loader: DocsLoader +): Promise { + const unstable_launchDarklySettings = await getLaunchDarklySettings( + loader.domain, + loader.getMetadata().then((metadata) => metadata.org) + ); + if (!unstable_launchDarklySettings) { + return undefined; + } + + return { + clientSideId: unstable_launchDarklySettings["client-side-id"], + contextEndpoint: unstable_launchDarklySettings["context-endpoint"], + context: undefined, + defaultFlags: undefined, + options: { + baseUrl: unstable_launchDarklySettings.options?.["base-url"], + streamUrl: unstable_launchDarklySettings.options?.["stream-url"], + eventsUrl: unstable_launchDarklySettings.options?.["events-url"], + hash: undefined, + }, + }; +} + +export async function generateViewport(props: { + params: Promise<{ domain: string }>; +}): Promise { + const { domain } = await props.params; + + const loader = await createCachedDocsLoader(domain); + const colors = await loader.getColors(); + const dark = maybeToHex( + colors.dark?.background ?? colors.dark?.accentPrimary + ); + const light = maybeToHex( + colors.light?.background ?? colors.light?.accentPrimary + ); + return { + themeColor: compact([ + dark ? { color: dark, media: "(prefers-color-scheme: dark)" } : undefined, + light + ? { color: light, media: "(prefers-color-scheme: light)" } + : undefined, + ]), + }; +} + +function maybeToHex(color: RgbaColor | undefined): string | undefined { + if (color == null) { + return undefined; + } + return tinycolor(color).toHexString(); +} + +export async function generateMetadata(props: { + params: Promise<{ domain: string }>; +}): Promise { + const { domain } = await props.params; + + const loader = await createCachedDocsLoader(domain); + const [files, config, seoDisabled] = await Promise.all([ + loader.getFiles(), + loader.getConfig(), + getSeoDisabled(domain), + ]); + + let index = config.metadata?.noindex ? false : undefined; + let follow = config.metadata?.nofollow ? false : undefined; + if (seoDisabled) { + index = false; + follow = false; + } + + return { + applicationName: config.title, + title: { + template: config.title ? "%s | " + config.title : "%s", + default: config.title ?? "Documentation", + }, + robots: { index, follow }, + openGraph: { + title: config.metadata?.["og:title"], + description: config.metadata?.["og:description"], + locale: config.metadata?.["og:locale"], + url: config.metadata?.["og:url"], + siteName: config.metadata?.["og:site_name"], + images: toImageDescriptor( + files, + config.metadata?.["og:image"], + config.metadata?.["og:image:width"], + config.metadata?.["og:image:height"] + ), + }, + twitter: { + site: config.metadata?.["twitter:site"], + creator: config.metadata?.["twitter:handle"], + title: config.metadata?.["twitter:title"], + description: config.metadata?.["twitter:description"], + images: toImageDescriptor(files, config.metadata?.["twitter:image"]), + }, + icons: { + icon: config.favicon + ? toImageDescriptor(files, { + type: "fileId", + value: config.favicon, + })?.url + : undefined, + }, + }; +} + +function generatePreloadHrefs( + typography: DocsV2Read.LoadDocsForUrlResponse["definition"]["config"]["typographyV2"], + files: Record +): void { + compact([ + typography?.bodyFont?.variants, + typography?.headingsFont?.variants, + typography?.codeFont?.variants, + ]) + .flat() + .map((variant) => files[variant.fontFile]?.src) + .filter(isNonNullish) + .forEach((src) => { + try { + preload(src, { + as: "font", + crossOrigin: "anonymous", + type: `font/${getFontExtension(src)}`, + fetchPriority: "high", + }); + } catch {} + }); +} + +function getFontExtension(url: string): string { + const ext = new URL(url).pathname.split(".").pop(); + if (ext == null) { + throw new Error("No extension found for font: " + url); + } + return ext; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/main.tsx b/packages/fern-docs/bundle/src/app/[domain]/main.tsx new file mode 100644 index 0000000000..85518196ac --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/main.tsx @@ -0,0 +1,99 @@ +import { notFound } from "next/navigation"; + +import { FernNavigation } from "@fern-api/fdr-sdk"; + +import ApiEndpointPage from "@/components/api-reference/ApiEndpointPage"; +import ChangelogEntryPage from "@/components/changelog/ChangelogEntryPage"; +import ChangelogPage from "@/components/changelog/ChangelogPage"; +import { LayoutEvaluator } from "@/components/layouts/LayoutEvaluator"; +import { DocsLoader } from "@/server/docs-loader"; +import { MdxSerializer } from "@/server/mdx-serializer"; + +import { BottomNavigation } from "./bottom-nav"; + +export async function DocsMainContent({ + loader, + serialize, + node, + parents, + neighbors, + breadcrumb, +}: { + loader: DocsLoader; + serialize: MdxSerializer; + node: FernNavigation.NavigationNodePage; + parents: readonly FernNavigation.NavigationNodeParent[]; + neighbors?: { + prev?: { + title: string; + href: string; + excerpt?: string; + }; + next?: { + title: string; + href: string; + excerpt?: string; + }; + }; + breadcrumb: readonly FernNavigation.BreadcrumbItem[]; +}) { + const bottomNavigation = neighbors && ( + + ); + + if (node.type === "changelog") { + return ( + + ); + } + + if (node.type === "changelogEntry") { + return ( + + ); + } + + if (FernNavigation.isApiLeaf(node)) { + return ( + + ); + } + + const pageId = FernNavigation.getPageId(node); + if (pageId != null) { + return ( + + ); + } + + console.error(`[${loader.domain}] Unknown node type: ${node.type}`); + notFound(); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/static/@headertabs/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/static/@headertabs/[slug]/page.tsx new file mode 100644 index 0000000000..62d785d13e --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/static/@headertabs/[slug]/page.tsx @@ -0,0 +1,31 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import { HeaderTabsList } from "@/components/header/HeaderTabsList"; +import { createCachedDocsLoader } from "@/server/docs-loader"; + +export default async function HeaderTabsPage({ + params, +}: { + params: Promise<{ domain: string; slug: string }>; +}) { + const { domain, slug } = await params; + const loader = await createCachedDocsLoader(domain); + const rootPromise = loader.getRoot(); + const layout = await loader.getLayout(); + + if (layout.tabsPlacement !== "HEADER") { + return null; + } + + const findNode = FernNavigation.utils.findNode( + await rootPromise, + slugjoin(slug) + ); + + if (findNode.type !== "found") { + return null; + } + + return ; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/static/@sidebar/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/static/@sidebar/[slug]/page.tsx new file mode 100644 index 0000000000..e78f56a2c6 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/static/@sidebar/[slug]/page.tsx @@ -0,0 +1,52 @@ +import { FernNavigation } from "@fern-api/fdr-sdk"; +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import { getFernToken } from "@/app/fern-token"; +import { SidebarTabsList } from "@/components/sidebar/SidebarTabsList"; +import { SidebarTabsRoot } from "@/components/sidebar/SidebarTabsRoot"; +import { SidebarRootNode } from "@/components/sidebar/nodes/SidebarRootNode"; +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { withPrunedNavigation } from "@/server/withPrunedNavigation"; + +export default async function SidebarPage({ + params, +}: { + params: Promise<{ domain: string; slug: string }>; +}) { + const { domain, slug } = await params; + const loader = await createCachedDocsLoader(domain); + const [root, layout, authState, edgeFlags] = await Promise.all([ + loader.getRoot(), + loader.getLayout(), + loader.getAuthState(), + loader.getEdgeFlags(), + ]); + + const foundNode = FernNavigation.utils.findNode(root, slugjoin(slug)); + if (foundNode.type !== "found") { + return null; + } + + // TODO: how do we handle hidden tabs? + const sidebar = withPrunedNavigation(foundNode.sidebar, { + visibleNodeIds: [foundNode.node.id], + authed: authState.authed, + // when true, all unauthed pages are visible, but rendered with a LOCK button + // so they're not actually "pruned" from the sidebar + // TODO: move this out of a feature flag and into the navigation node metadata + discoverable: edgeFlags.isAuthenticatedPagesDiscoverable + ? (true as const) + : undefined, + }); + + return ( + <> + {foundNode.tabs && foundNode.tabs.length > 0 && ( + + + + )} + {sidebar && } + + ); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/loading.tsx b/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/loading.tsx new file mode 100644 index 0000000000..2b92e7bb4d --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import LoadingDocs from "@/components/shared-loading"; + +export default function Loading() { + return ; +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/page.tsx b/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/page.tsx new file mode 100644 index 0000000000..bc8bbd6673 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/static/[slug]/page.tsx @@ -0,0 +1,29 @@ +import "server-only"; + +import { Metadata } from "next/types"; + +import { slugjoin } from "@fern-api/fdr-sdk/navigation"; + +import SharedPage, { + generateMetadata as _generateMetadata, +} from "@/components/shared-page"; + +export const dynamic = "force-static"; + +export default async function StaticPage({ + params, +}: { + params: Promise<{ slug: string; domain: string }>; +}) { + const { domain, slug } = await params; + return ; +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string; domain: string }>; +}): Promise { + const { domain, slug } = await params; + return _generateMetadata({ domain, slug: slugjoin(slug) }); +} diff --git a/packages/fern-docs/bundle/src/app/[domain]/static/layout.tsx b/packages/fern-docs/bundle/src/app/[domain]/static/layout.tsx new file mode 100644 index 0000000000..feb1770b5d --- /dev/null +++ b/packages/fern-docs/bundle/src/app/[domain]/static/layout.tsx @@ -0,0 +1,20 @@ +import SharedLayout from "@/components/shared-layout"; + +export default async function Layout({ + children, + params, + headertabs, + sidebar, +}: { + children: React.ReactNode; + params: Promise<{ domain: string }>; + headertabs: React.ReactNode; + sidebar: React.ReactNode; +}) { + const { domain } = await params; + return ( + + {children} + + ); +} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/endpoint/[endpoint]/route.ts b/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/endpoint/[endpoint]/route.ts deleted file mode 100644 index 1a74efc275..0000000000 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/endpoint/[endpoint]/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAuthStateEdge } from "@/server/auth/getAuthStateEdge"; -import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; -import { ApiDefinitionLoader } from "@fern-docs/cache"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { getMdxBundler } from "@fern-docs/ui/bundlers"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET( - req: NextRequest, - { params }: { params: { api: string; endpoint: string } } -): Promise { - const { api, endpoint } = params; - - const authState = await getAuthStateEdge(req); - - if (!authState.ok) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: authState.authed ? 403 : 401 } - ); - } - - const flags = await getEdgeFlags(authState.domain); - const engine = flags.useMdxBundler ? "mdx-bundler" : "next-mdx-remote"; - const serializeMdx = await getMdxBundler(engine); - - const apiDefinition = await ApiDefinitionLoader.create( - authState.domain, - ApiDefinition.ApiDefinitionId(api) - ) - .withEdgeFlags(flags) - .withMdxBundler(serializeMdx, engine) - .withPrune({ - type: "endpoint", - endpointId: ApiDefinition.EndpointId(endpoint), - }) - .withResolveDescriptions() - .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) - .load(); - - if (!apiDefinition) { - return NextResponse.json( - { error: "API Definition not found" }, - { status: 404 } - ); - } - - const response = NextResponse.json(apiDefinition, { status: 200 }); - response.headers.set( - "Cache-Control", - "public, s-maxage=3600, stale-while-revalidate=86400" - ); - - return response; -} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/webhook/[webhook]/route.ts b/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/webhook/[webhook]/route.ts deleted file mode 100644 index 4f5472159f..0000000000 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/webhook/[webhook]/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAuthStateEdge } from "@/server/auth/getAuthStateEdge"; -import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; -import { ApiDefinitionLoader } from "@fern-docs/cache"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { getMdxBundler } from "@fern-docs/ui/bundlers"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET( - req: NextRequest, - { params }: { params: { api: string; webhook: string } } -): Promise { - const { api, webhook } = params; - - const authState = await getAuthStateEdge(req); - - if (!authState.ok) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: authState.authed ? 403 : 401 } - ); - } - - const flags = await getEdgeFlags(authState.domain); - const engine = flags.useMdxBundler ? "mdx-bundler" : "next-mdx-remote"; - const serializeMdx = await getMdxBundler(engine); - - const apiDefinition = await ApiDefinitionLoader.create( - authState.domain, - ApiDefinition.ApiDefinitionId(api) - ) - .withEdgeFlags(flags) - .withMdxBundler(serializeMdx, engine) - .withPrune({ - type: "webhook", - webhookId: ApiDefinition.WebhookId(webhook), - }) - .withResolveDescriptions() - .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) - .load(); - - if (!apiDefinition) { - return NextResponse.json( - { error: "API Definition not found" }, - { status: 404 } - ); - } - - const response = NextResponse.json(apiDefinition, { status: 200 }); - response.headers.set( - "Cache-Control", - "public, s-maxage=3600, stale-while-revalidate=86400" - ); - - return response; -} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/websocket/[websocket]/route.ts b/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/websocket/[websocket]/route.ts deleted file mode 100644 index b218c5617f..0000000000 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/api-definition/[api]/websocket/[websocket]/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAuthStateEdge } from "@/server/auth/getAuthStateEdge"; -import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; -import { ApiDefinitionLoader } from "@fern-docs/cache"; -import { getEdgeFlags } from "@fern-docs/edge-config"; -import { getMdxBundler } from "@fern-docs/ui/bundlers"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET( - req: NextRequest, - { params }: { params: { api: string; websocket: string } } -): Promise { - const { api, websocket } = params; - - const authState = await getAuthStateEdge(req); - - if (!authState.ok) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: authState.authed ? 403 : 401 } - ); - } - - const flags = await getEdgeFlags(authState.domain); - const engine = flags.useMdxBundler ? "mdx-bundler" : "next-mdx-remote"; - const serializeMdx = await getMdxBundler(engine); - - const apiDefinition = await ApiDefinitionLoader.create( - authState.domain, - ApiDefinition.ApiDefinitionId(api) - ) - .withEdgeFlags(flags) - .withMdxBundler(serializeMdx, engine) - .withPrune({ - type: "webSocket", - webSocketId: ApiDefinition.WebSocketId(websocket), - }) - .withResolveDescriptions() - .withEnvironment(process.env.NEXT_PUBLIC_FDR_ORIGIN) - .load(); - - if (!apiDefinition) { - return NextResponse.json( - { error: "API Definition not found" }, - { status: 404 } - ); - } - - const response = NextResponse.json(apiDefinition, { status: 200 }); - response.headers.set( - "Cache-Control", - "public, s-maxage=3600, stale-while-revalidate=86400" - ); - - return response; -} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v1/key/route.ts b/packages/fern-docs/bundle/src/app/api/fern-docs/search/v1/key/route.ts deleted file mode 100644 index 9b1503a9bb..0000000000 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/search/v1/key/route.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { getAuthStateEdge } from "@/server/auth/getAuthStateEdge"; -import { loadWithUrl } from "@/server/loadWithUrl"; -import { getInkeepSettings } from "@fern-docs/edge-config"; -import { SearchConfig, getSearchConfig } from "@fern-docs/search-utils"; -import { provideRegistryService } from "@fern-docs/ui"; -import { NextRequest, NextResponse } from "next/server"; - -export const runtime = "nodejs"; - -export async function GET( - req: NextRequest -): Promise> { - const authState = await getAuthStateEdge(req, req.nextUrl.pathname); - - if (!authState.ok) { - return NextResponse.json( - { isAvailable: false }, - { status: authState.authed ? 403 : 401 } - ); - } - - const docs = await loadWithUrl(authState.domain); - - if (!docs.ok) { - return NextResponse.json({ isAvailable: false }, { status: 503 }); - } - - const inkeepSettings = await getInkeepSettings(authState.domain); - const searchInfo = docs.body.definition.search; - const config = await getSearchConfig( - provideRegistryService(), - searchInfo, - inkeepSettings - ); - - return NextResponse.json(config, { - status: config.isAvailable ? 200 : 503, - }); -} diff --git a/packages/fern-docs/bundle/src/app/api/fern-docs/upload/route.ts b/packages/fern-docs/bundle/src/app/api/fern-docs/upload/route.ts deleted file mode 100644 index 5c9ebc5ad8..0000000000 --- a/packages/fern-docs/bundle/src/app/api/fern-docs/upload/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - createGetSignedUrl, - createPutSignedUrl, -} from "@/server/createSignedUrl"; -import { NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; -export const maxDuration = 5; - -export async function GET(req: NextRequest): Promise { - const corsHeaders = new Headers({ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }); - - const domain = req.nextUrl.hostname; - const time: string = new Date().toISOString(); - const file = req.nextUrl.searchParams.get("file"); - - if (file == null) { - return new NextResponse(null, { status: 400 }); - } - - try { - const key = constructS3Key(domain, time, file); - const [put, get] = await Promise.all([ - await createPutSignedUrl(key, 60), // 1 minute - await createGetSignedUrl(key, 60 * 5), // 5 minutes - ]); - corsHeaders.set("Cache-Control", "public, max-age=60"); - return NextResponse.json({ put, get }, { headers: corsHeaders }); - } catch (err) { - console.error("Failed to create signed URL", err); - return new NextResponse(null, { status: 500, headers: corsHeaders }); - } -} - -function constructS3Key(domain: string, time: string, file: string): string { - return `${domain}/user-upload/${time}/${file}`; -} - -export async function OPTIONS(): Promise { - const corsHeaders = new Headers({ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type", - }); - - return new NextResponse(null, { status: 204, headers: corsHeaders }); -} diff --git a/packages/fern-docs/bundle/src/app/error.tsx b/packages/fern-docs/bundle/src/app/error.tsx new file mode 100644 index 0000000000..cc91e67ba4 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/error.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { ErrorBoundaryFallback } from "@/components/error-boundary"; + +export default function ErrorBoundary({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + console.error(error); + return ; +} diff --git a/packages/fern-docs/bundle/src/app/fern-token.ts b/packages/fern-docs/bundle/src/app/fern-token.ts new file mode 100644 index 0000000000..29329a71b1 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/fern-token.ts @@ -0,0 +1,10 @@ +import "server-only"; + +import { cookies } from "next/headers"; + +import { COOKIE_FERN_TOKEN } from "@fern-docs/utils"; + +export async function getFernToken() { + const cookieJar = await cookies(); + return cookieJar.get(COOKIE_FERN_TOKEN)?.value; +} diff --git a/packages/fern-docs/bundle/src/app/global-error.tsx b/packages/fern-docs/bundle/src/app/global-error.tsx new file mode 100644 index 0000000000..9edad3a86e --- /dev/null +++ b/packages/fern-docs/bundle/src/app/global-error.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { ErrorBoundaryFallback } from "@/components/error-boundary"; + +export default function ErrorBoundary({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + + + + + + ); +} diff --git a/packages/fern-docs/bundle/src/app/global-styles.tsx b/packages/fern-docs/bundle/src/app/global-styles.tsx new file mode 100644 index 0000000000..9e76125ae5 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/global-styles.tsx @@ -0,0 +1,39 @@ +"use client"; + +export function GlobalStyles({ + children, + domain, + layout, +}: { + children: string; + domain: string; + layout: { + logoHeight: number; + sidebarWidth: number; + headerHeight: number; + pageWidth: number | undefined; + contentWidth: number; + tabsPlacement: "SIDEBAR" | "HEADER"; + searchbarPlacement: "SIDEBAR" | "HEADER" | "HEADER_TABS"; + }; +}) { + return ( + + ); +} diff --git a/packages/fern-docs/bundle/src/app/globals.css b/packages/fern-docs/bundle/src/app/globals.css new file mode 100644 index 0000000000..269ec8e67c --- /dev/null +++ b/packages/fern-docs/bundle/src/app/globals.css @@ -0,0 +1,45 @@ +@import "tailwindcss"; +@import "../components/css/rmiz.scss"; +@import "../components/css/base.scss"; +@import "@fern-docs/components/src/index.css"; +@import "@fern-docs/syntax-highlighter/src/index.css"; +@import "@fern-docs/search-ui/src/index.css"; +@import "../components/css/components.scss"; +@import "../components/api-reference/index.scss"; +@import "../components/playground/index.scss"; +@import "../components/components/index.scss"; +@import "../components/mdx/components/index.scss"; +@import "../components/changelog/index.scss"; +@import "../components/themes/index.scss"; +@import "../components/sidebar/index.scss"; + +@plugin "@tailwindcss/typography"; +@config "../../../components/tailwind.config.ts"; + +@source "../components"; +@source "../../../components/src"; +@source "../../../search-ui/src/components"; +@source "../../../syntax-highlighter/src"; + +:root { + --page-padding: 1rem; +} + +@media (min-width: 768px) { + :root { + --page-padding: 1.5rem; + } +} + +@media (min-width: 1024px) { + :root { + --page-padding: 2rem; + } +} + +@theme inline { + --spacing-page-padding: var(--page-padding); + --spacing-page-width-padded: calc( + var(--spacing-page-width) + var(--spacing-page-padding) * 2 + ); +} diff --git a/packages/fern-docs/bundle/src/app/layout.tsx b/packages/fern-docs/bundle/src/app/layout.tsx new file mode 100644 index 0000000000..2ea6adbe99 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/layout.tsx @@ -0,0 +1,88 @@ +import { Metadata, Viewport } from "next/types"; +import { experimental_taintUniqueValue } from "react"; + +import { TooltipProvider } from "@radix-ui/react-tooltip"; + +import { Toaster } from "@fern-docs/components"; + +import { ConsoleMessage } from "@/components/console-message"; +import { JotaiProvider } from "@/state/jotai-provider"; + +import "./globals.css"; +import StyledJsxRegistry from "./registry"; + +const secrets = [ + "BRAINTRUST_API_KEY", + "ALGOLIA_API_KEY", + "ALGOLIA_SEARCH_API_KEY", + "ALGOLIA_WRITE_API_KEY", + "ANTHROPIC_API_KEY", + "AWS_SECRET_ACCESS_KEY", + "COHERE_API_KEY", + "EDGE_CONFIG", + "FERN_TOKEN", + "JWT_SECRET_KEY", + "KV_REST_API_READ_ONLY_TOKEN", + "KV_REST_API_TOKEN", + "OPENAI_API_KEY", + "QSTASH_CURRENT_SIGNING_KEY", + "QSTASH_NEXT_SIGNING_KEY", + "QSTASH_TOKEN", + "TURBOPUFFER_API_KEY", + "WORKOS_API_KEY", + "HIGHLIGHT_PROJECT_ID_FERN_APP", + "VERCEL_AUTOMATION_BYPASS_SECRET", +]; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + for (const secret of secrets) { + const secretValue = process.env[secret]; + if (secretValue != null) { + experimental_taintUniqueValue( + `Do not pass ${secret} to the client.`, + process.env, + secretValue + ); + } + } + + return ( + + + + + + + + + + + {children} + + + + + + ); +} + +export const viewport: Viewport = { + width: "device-width", + height: "device-height", + initialScale: 1, + maximumScale: 1, + minimumScale: 1, + userScalable: true, +}; + +export const metadata: Metadata = { + generator: "https://buildwithfern.com", +}; diff --git a/packages/fern-docs/bundle/src/app/manifest.ts b/packages/fern-docs/bundle/src/app/manifest.ts deleted file mode 100644 index 621dbf0c9b..0000000000 --- a/packages/fern-docs/bundle/src/app/manifest.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { loadWithUrl } from "@/server/loadWithUrl"; -import { getDocsDomainApp } from "@/server/xfernhost/app"; -import { DocsV1Read } from "@fern-api/fdr-sdk"; -import { isNonNullish } from "@fern-api/ui-core-utils"; -import { addLeadingSlash } from "@fern-docs/utils"; -import type { MetadataRoute } from "next"; -import { notFound } from "next/navigation"; - -export default async function manifest(): Promise { - const domain = getDocsDomainApp(); - - const docs = await loadWithUrl(domain); - - if (!docs.ok) { - notFound(); - } - - const favicon = selectFile( - docs.body.definition.filesV2, - docs.body.definition.config.favicon - ); - - return { - name: docs.body.definition.config.title ?? "Documentation", - start_url: addLeadingSlash(docs.body.baseUrl.basePath ?? ""), - display: "browser", - icons: [ - favicon != null - ? { src: favicon, sizes: "any", type: "image/x-icon" } - : undefined, - ].filter(isNonNullish), - }; -} - -function selectFile( - files: Record, - fileId: DocsV1Read.FileId | undefined -) { - if (!fileId) { - return undefined; - } - return files[fileId]?.url; -} diff --git a/packages/fern-docs/bundle/src/app/registry.tsx b/packages/fern-docs/bundle/src/app/registry.tsx new file mode 100644 index 0000000000..93d3e13e4b --- /dev/null +++ b/packages/fern-docs/bundle/src/app/registry.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useServerInsertedHTML } from "next/navigation"; +import React, { useState } from "react"; + +import { StyleRegistry, createStyleRegistry } from "styled-jsx"; + +export default function StyledJsxRegistry({ + children, +}: { + children: React.ReactNode; +}) { + // Only create stylesheet once with lazy initial state + // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state + const [jsxStyleRegistry] = useState(() => createStyleRegistry()); + + useServerInsertedHTML(() => { + const styles = jsxStyleRegistry.styles(); + jsxStyleRegistry.flush(); + return <>{styles}; + }); + + return {children}; +} diff --git a/packages/fern-docs/bundle/src/app/robots.ts b/packages/fern-docs/bundle/src/app/robots.ts index 292702a3a0..bb78dfd975 100644 --- a/packages/fern-docs/bundle/src/app/robots.ts +++ b/packages/fern-docs/bundle/src/app/robots.ts @@ -1,18 +1,32 @@ -import { getDocsDomainApp, getHostApp } from "@/server/xfernhost/app"; -import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -import { getSeoDisabled } from "@fern-docs/edge-config"; import type { MetadataRoute } from "next"; import { headers } from "next/headers"; + import urlJoin from "url-join"; -export const dynamic = "force-dynamic"; +import { withDefaultProtocol } from "@fern-api/ui-core-utils"; +import { getSeoDisabled } from "@fern-docs/edge-config"; +import { HEADER_HOST, HEADER_X_FERN_HOST } from "@fern-docs/utils"; + export const runtime = "edge"; export default async function robots(): Promise { - const domain = getDocsDomainApp(); - const host = getHostApp() ?? domain; - const basepath = headers().get("x-fern-basepath") ?? ""; - const sitemap = urlJoin(withDefaultProtocol(host), basepath, "/sitemap.xml"); + const headersList = await headers(); + const domain = + headersList.get(HEADER_X_FERN_HOST) ?? headersList.get(HEADER_HOST); + if (!domain) { + return { + rules: { + userAgent: "*", + disallow: "/", + }, + }; + } + const basepath = headersList.get("x-fern-basepath") ?? ""; + const sitemap = urlJoin( + withDefaultProtocol(domain), + basepath, + "/sitemap.xml" + ); if (await getSeoDisabled(domain)) { return { @@ -21,7 +35,7 @@ export default async function robots(): Promise { disallow: "/", }, sitemap, - host, + host: domain, }; } @@ -31,6 +45,6 @@ export default async function robots(): Promise { allow: "/", }, sitemap, - host, + host: domain, }; } diff --git a/packages/fern-docs/bundle/src/app/seo.ts b/packages/fern-docs/bundle/src/app/seo.ts new file mode 100644 index 0000000000..7c8c6f9693 --- /dev/null +++ b/packages/fern-docs/bundle/src/app/seo.ts @@ -0,0 +1,30 @@ +import { FileIdOrUrl } from "@fern-api/fdr-sdk/docs"; + +export function toImageDescriptor( + files: Record< + string, + { src: string; width?: number; height?: number; alt?: string } + >, + image: FileIdOrUrl | undefined, + width?: number, + height?: number +): { url: string; width?: number; height?: number; alt?: string } | undefined { + if (image == null) { + return undefined; + } + + if (image.type === "url") { + return { url: image.value }; + } + + const file = files[image.value]; + if (file == null) { + return undefined; + } + return { + url: file.src, + width: width ?? file.width, + height: height ?? file.height, + alt: file.alt, + }; +} diff --git a/packages/fern-docs/bundle/src/app/sitemap.ts b/packages/fern-docs/bundle/src/app/sitemap.ts index 0f7a476b24..4449da42ed 100644 --- a/packages/fern-docs/bundle/src/app/sitemap.ts +++ b/packages/fern-docs/bundle/src/app/sitemap.ts @@ -1,22 +1,18 @@ -import { DocsLoader } from "@/server/DocsLoader"; -import { withPrunedNavigation } from "@/server/withPrunedNavigation"; -import { getDocsDomainApp, getHostApp } from "@/server/xfernhost/app"; +import type { MetadataRoute } from "next"; + +import urljoin from "url-join"; + import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { conformTrailingSlash } from "@fern-docs/utils"; -import type { MetadataRoute } from "next"; -import urljoin from "url-join"; -export const dynamic = "force-dynamic"; +import { createCachedDocsLoader } from "@/server/docs-loader"; +import { getDocsDomainApp } from "@/server/xfernhost/app"; export default async function sitemap(): Promise { - const domain = getDocsDomainApp(); - const host = getHostApp() ?? domain; - - // load the root node, and prune it— sitemap should only include public routes - const root = withPrunedNavigation(await DocsLoader.for(domain, host).root(), { - authed: false, - }); + const domain = await getDocsDomainApp(); + const loader = await createCachedDocsLoader(domain); + const root = await loader.getRoot(); // collect all indexable page slugs const slugs = NodeCollector.collect(root).indexablePageSlugs; @@ -26,8 +22,5 @@ export default async function sitemap(): Promise { conformTrailingSlash(urljoin(withDefaultProtocol(domain), slug)) ); - // TODO: update lastModified to be the date of the last commit to the page - // and add a changeFrequency of "daily", unless it's a changelog page or blog post - return [...urls.map((url) => ({ url }))]; } diff --git a/packages/fern-docs/bundle/src/app/theme.tsx b/packages/fern-docs/bundle/src/app/theme.tsx new file mode 100644 index 0000000000..cceabbabde --- /dev/null +++ b/packages/fern-docs/bundle/src/app/theme.tsx @@ -0,0 +1,32 @@ +import { ThemeProvider as NextThemeProvider } from "next-themes"; + +export function ThemeProvider({ + children, + hasLight, + hasDark, +}: { + children: React.ReactNode; + hasLight: boolean; + hasDark: boolean; +}) { + const enableSystem = hasLight === hasDark; + const forcedTheme = enableSystem + ? undefined + : hasLight + ? "light" + : hasDark + ? "dark" + : undefined; + + return ( + + {children} + + ); +} diff --git a/packages/fern-docs/bundle/src/components/__test__/setup.ts b/packages/fern-docs/bundle/src/components/__test__/setup.ts new file mode 100644 index 0000000000..63eb9f4d1f --- /dev/null +++ b/packages/fern-docs/bundle/src/components/__test__/setup.ts @@ -0,0 +1,10 @@ +import * as matchers from "@testing-library/jest-dom/matchers"; +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach, expect } from "vitest"; + +expect.extend(matchers); + +afterEach(() => { + cleanup(); +}); diff --git a/packages/fern-docs/bundle/src/components/analytics/CustomerAnalytics.tsx b/packages/fern-docs/bundle/src/components/analytics/CustomerAnalytics.tsx new file mode 100644 index 0000000000..ce4ca555e3 --- /dev/null +++ b/packages/fern-docs/bundle/src/components/analytics/CustomerAnalytics.tsx @@ -0,0 +1,37 @@ +"use client"; + +import dynamic from "next/dynamic"; +import React from "react"; + +import { DocsV1Read } from "@fern-api/fdr-sdk"; + +import { PosthogProvider } from "./posthog-provider"; + +const FullstoryScript = dynamic(() => import("./FullstoryScript"), { + ssr: true, +}); +const GoogleAnalytics = dynamic(() => import("./ga"), { ssr: true }); +const GoogleTagManager = dynamic(() => import("./gtm"), { ssr: true }); +const IntercomScript = dynamic(() => import("./intercom"), { ssr: true }); +const SegmentScript = dynamic(() => import("./segment"), { ssr: true }); + +export function CustomerAnalytics({ + config, +}: { + config?: Partial; +}) { + if (!config) { + return null; + } + + return ( + <> + + {config.fullstory && } + {config.ga4 && } + {config.gtm && } + {config.intercom && } + {config.segment && } + + ); +} diff --git a/packages/fern-docs/ui/src/analytics/FullstoryScript.tsx b/packages/fern-docs/bundle/src/components/analytics/FullstoryScript.tsx similarity index 94% rename from packages/fern-docs/ui/src/analytics/FullstoryScript.tsx rename to packages/fern-docs/bundle/src/components/analytics/FullstoryScript.tsx index 1c4f40f059..0d17a8cc92 100644 --- a/packages/fern-docs/ui/src/analytics/FullstoryScript.tsx +++ b/packages/fern-docs/bundle/src/components/analytics/FullstoryScript.tsx @@ -1,14 +1,11 @@ -import { DocsV1Read } from "@fern-api/fdr-sdk"; import Script from "next/script"; import { ReactElement } from "react"; -export function FullstoryScript(props: { - config?: DocsV1Read.FullStoryAnalyticsConfig; -}): ReactElement { - if (!props.config) { - return <>; - } +import { DocsV1Read } from "@fern-api/fdr-sdk"; +function FullstoryScript(props: { + config: DocsV1Read.FullStoryAnalyticsConfig; +}): ReactElement { return ( + ))} + {config.remote?.map((remote) => ( + - ))} - {js?.remote?.map((remote) => ( -