diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml index c006a0808..9d9ab4c1d 100644 --- a/.github/actions/deploy/action.yml +++ b/.github/actions/deploy/action.yml @@ -12,21 +12,39 @@ inputs: website-ref: description: Website Git revision associated with the deployment required: true - mxbai-api-key: - description: Mixedbread API key + mxbai-admin-api-key: + description: Mixedbread production synchronization API key + required: true + mxbai-search-api-key: + description: Mixedbread read-only Worker search API key required: true mxbai-vector-store-id: description: Mixedbread vector store ID required: true - vercel-org-id: - description: Vercel organization ID + cloudflare-account-id: + description: Cloudflare account ID + required: true + cloudflare-api-token: + description: Cloudflare API token required: true - vercel-project-id: - description: Vercel project ID + cloudflare-traffic-mode: + description: Cloudflare production traffic mode + required: false + default: workers-dev + kv-rest-api-url: + description: Upstash REST API URL required: true - vercel-token: - description: Vercel authentication token + kv-rest-api-token: + description: Upstash REST API token required: true + public-posthog-key: + description: Public PostHog project key + required: false + default: "" + public-posthog-api-host: + description: Public PostHog API host + required: false + default: https://us.i.posthog.com runs: using: composite @@ -41,21 +59,9 @@ runs: GH_TOKEN: ${{ github.token }} REQUESTED_SNAPSHOT: ${{ inputs.api-reference-snapshot }} run: | - snapshot="$REQUESTED_SNAPSHOT" - if [[ -z "$snapshot" ]]; then - snapshot=$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --jq '[.[] | select(.draft == false and .prerelease == false and (.tag_name | startswith("api-reference-")))] | sort_by(.published_at) | last | (.tag_name // "")') - fi - - if [[ ! "$snapshot" =~ ^api-reference-[a-f0-9]{64}$ ]]; then - echo "::error::No valid published API reference snapshot is available. Publish one with the API reference workflow before deploying." - exit 1 - fi - - release=$(gh release view "$snapshot" --json isDraft,isPrerelease) - if [[ "$(jq -r '.isDraft or .isPrerelease' <<<"$release")" == "true" ]]; then - echo "::error::API reference snapshot must be a published, non-prerelease release: $snapshot" - exit 1 - fi + snapshot=$(vp exec api-reference snapshot resolve \ + --repository "$GITHUB_REPOSITORY" \ + --tag "$REQUESTED_SNAPSHOT") echo "tag=$snapshot" >> "$GITHUB_OUTPUT" - name: Restore API reference snapshot @@ -90,38 +96,34 @@ runs: exit 1 fi - - name: Pull Vercel Environment Information - shell: bash - run: pnpm exec vercel pull --cwd apps/web --yes --environment=production --token="${{ inputs.vercel-token }}" --scope=effect-ts - env: - VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} - VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} - - - name: Build Project Artifacts + - name: Deploy website to Cloudflare shell: bash - run: pnpm exec vercel build --cwd apps/web --prod --token="${{ inputs.vercel-token }}" + run: vp exec alchemy deploy --stage prod --yes env: - VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} - VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + CLOUDFLARE_API_TOKEN: ${{ inputs.cloudflare-api-token }} + CLOUDFLARE_ACCOUNT_ID: ${{ inputs.cloudflare-account-id }} + CLOUDFLARE_TRAFFIC_MODE: ${{ inputs.cloudflare-traffic-mode }} + MXBAI_API_KEY: ${{ inputs.mxbai-search-api-key }} + MXBAI_VECTOR_STORE_ID: ${{ inputs.mxbai-vector-store-id }} + KV_REST_API_URL: ${{ inputs.kv-rest-api-url }} + KV_REST_API_TOKEN: ${{ inputs.kv-rest-api-token }} + PUBLIC_POSTHOG_KEY: ${{ inputs.public-posthog-key }} + PUBLIC_POSTHOG_API_HOST: ${{ inputs.public-posthog-api-host }} + GITHUB_TOKEN: ${{ github.token }} + GITHUB_SHA: ${{ inputs.website-ref }} - name: Verify generated API reference links shell: bash - run: node apps/web/scripts/verify-api-reference-links.mjs - - - name: Patch trailing-slash redirect into build output - shell: bash - run: node apps/web/scripts/patch-vercel-trailing-slash.mjs - - - name: Deploy Project Artifacts to Vercel - shell: bash - run: pnpm exec vercel deploy apps/web --prebuilt --archive=tgz --prod --token="${{ inputs.vercel-token }}" --scope=effect-ts - env: - VERCEL_ORG_ID: ${{ inputs.vercel-org-id }} - VERCEL_PROJECT_ID: ${{ inputs.vercel-project-id }} + run: | + if [[ -d apps/web/dist/client/docs ]]; then + node apps/web/scripts/verify-api-reference-links.mjs apps/web/dist/client + else + echo "Alchemy reused the unchanged deployment; no local build output to verify." + fi - name: Synchronize Mixedbread production store shell: bash run: vp exec mixedbread sync --scope "${{ inputs.mixedbread-scope }}" --sha "${{ inputs.website-ref }}" env: - MXBAI_API_KEY: ${{ inputs.mxbai-api-key }} + MXBAI_ADMIN_API_KEY: ${{ inputs.mxbai-admin-api-key }} MXBAI_VECTOR_STORE_ID: ${{ inputs.mxbai-vector-store-id }} diff --git a/.github/actions/prepare-api-reference/action.yml b/.github/actions/prepare-api-reference/action.yml new file mode 100644 index 000000000..c30985b40 --- /dev/null +++ b/.github/actions/prepare-api-reference/action.yml @@ -0,0 +1,52 @@ +name: Prepare API reference +description: Check out Effect v3 and v4 and generate their API reference data. + +inputs: + cache: + description: Restore and save generated API reference data + required: false + default: "false" + v3-revision: + description: Effect v3 Git revision + required: false + default: v3 + v4-revision: + description: Effect v4 Git revision + required: false + default: main + +runs: + using: composite + steps: + - name: Checkout Effect v3 + id: checkout-v3 + uses: actions/checkout@v6 + with: + repository: Effect-TS/effect + ref: ${{ inputs.v3-revision }} + path: .effect-source-v3 + fetch-depth: 1 + + - name: Checkout Effect v4 + id: checkout-v4 + uses: actions/checkout@v6 + with: + repository: Effect-TS/effect + ref: ${{ inputs.v4-revision }} + path: .effect-source-v4 + fetch-depth: 1 + + - name: Restore API reference cache + if: inputs.cache == 'true' + id: cache + uses: actions/cache@v5 + with: + path: apps/web/.data/api-reference + key: api-reference-v2-${{ runner.os }}-v3-${{ steps.checkout-v3.outputs.commit }}-v4-${{ steps.checkout-v4.outputs.commit }}-${{ hashFiles('packages/api-reference/src/Generate.ts', 'packages/api-reference/package.json', 'packages/domain/src/ApiReference.ts', 'pnpm-lock.yaml') }} + + - name: Generate API reference + if: inputs.cache != 'true' || steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + vp run api-reference:generate --version v3 --repo .effect-source-v3 + vp run api-reference:generate --version v4 --repo .effect-source-v4 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 14f1487d8..49f410549 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -1,12 +1,5 @@ name: Setup description: Perform standard setup and install dependencies using pnpm. -inputs: - deno-version: - description: The version of Deno to install - required: false - bun-version: - description: The version of Bun to install - required: false runs: using: composite @@ -22,16 +15,6 @@ runs: uses: voidzero-dev/setup-vp@v1.17.0 with: node-manager: false - - name: Install deno - uses: denoland/setup-deno@v2 - if: ${{ inputs.deno-version != '' }} - with: - deno-version: ${{ inputs.deno-version }} - - name: Install bun - uses: oven-sh/setup-bun@v2 - if: ${{ inputs.bun-version != '' }} - with: - bun-version: ${{ inputs.bun-version }} - name: Install dependencies shell: bash - run: pnpm install + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ac5c6130a..927041670 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,8 +14,8 @@ concurrency: permissions: {} jobs: - lint: - name: Lint + check: + name: Check runs-on: ubuntu-latest permissions: contents: read @@ -24,22 +24,10 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - - run: vp run lint - - format: - name: Format - runs-on: ubuntu-latest - permissions: - contents: read - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - name: Install dependencies - uses: ./.github/actions/setup - - run: vp run fmt + - run: vp run check - types: - name: Types + test: + name: Test runs-on: ubuntu-latest permissions: contents: read @@ -48,4 +36,4 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - - run: vp run check + - run: vp test diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..f153ea140 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,69 @@ +name: Deploy + +on: + workflow_call: + inputs: + website-ref: + description: Website Git revision to deploy + required: true + type: string + api-reference-snapshot: + description: Exact API reference snapshot tag; latest published snapshot when omitted + required: false + default: "" + type: string + mixedbread-scope: + description: Mixedbread content scope to synchronize + required: false + default: markdown + type: string + cloudflare-traffic-mode: + description: Cloudflare production traffic mode + required: false + default: workers-dev + type: string + secrets: + MXBAI_ADMIN_API_KEY: + required: true + MXBAI_SEARCH_API_KEY: + required: true + MXBAI_VECTOR_STORE_ID: + required: true + CLOUDFLARE_ACCOUNT_ID: + required: true + CLOUDFLARE_API_TOKEN: + required: true + KV_REST_API_URL: + required: true + KV_REST_API_TOKEN: + required: true + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + concurrency: + group: production + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.website-ref }} + + - uses: ./.github/actions/deploy + with: + website-ref: ${{ inputs.website-ref }} + api-reference-snapshot: ${{ inputs.api-reference-snapshot }} + mixedbread-scope: ${{ inputs.mixedbread-scope }} + mxbai-admin-api-key: ${{ secrets.MXBAI_ADMIN_API_KEY }} + mxbai-search-api-key: ${{ secrets.MXBAI_SEARCH_API_KEY }} + mxbai-vector-store-id: ${{ secrets.MXBAI_VECTOR_STORE_ID }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-traffic-mode: ${{ inputs.cloudflare-traffic-mode }} + kv-rest-api-url: ${{ secrets.KV_REST_API_URL }} + kv-rest-api-token: ${{ secrets.KV_REST_API_TOKEN }} + public-posthog-key: ${{ vars.PUBLIC_POSTHOG_KEY }} + public-posthog-api-host: ${{ vars.PUBLIC_POSTHOG_API_HOST }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 3d1213f87..9c0f865b9 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -1,4 +1,4 @@ -name: GitHub Actions Vercel Preview Deployment +name: Preview Deployment on: pull_request: @@ -17,119 +17,57 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + pull-requests: write steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Detect search index changes id: search-changes env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - changed=false - - while IFS= read -r file; do - case "$file" in - apps/web/src/content/docs/*|\ - apps/web/src/content/blog/*|\ - apps/web/src/content.config.ts|\ - packages/domain/*|\ - packages/api-reference/*|\ - packages/mixedbread/*|\ - apps/web/src/features/api-reference/*|\ - apps/web/src/services/search/domain.ts|\ - apps/web/package.json|\ - packages/mixedbread/package.json|\ - package.json|\ - pnpm-lock.yaml|\ - .github/workflows/preview.yml) - changed=true - break - ;; - esac - done < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") - + changed=$(vp exec mixedbread search-changes --base "$BASE_SHA" --head "$HEAD_SHA") echo "changed=$changed" >> "$GITHUB_OUTPUT" - - name: Install dependencies - uses: ./.github/actions/setup - - - name: Checkout Effect v3 - uses: actions/checkout@v6 - with: - repository: Effect-TS/effect - ref: adf8a89decd1552b692710bf218ada12671af2ef - path: .effect-source-v3 - fetch-depth: 1 - - - name: Checkout Effect v4 - uses: actions/checkout@v6 - with: - repository: Effect-TS/effect - ref: 52838418db2e04db6aaed2fa01b280f2aad4032a - path: .effect-source-v4 - fetch-depth: 1 - - - name: Resolve Effect revisions - id: effect-revisions - run: | - echo "v3=$(git -C .effect-source-v3 rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "v4=$(git -C .effect-source-v4 rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - - name: Restore API reference cache - id: api-reference-cache - uses: actions/cache@v5 + - name: Prepare API reference + uses: ./.github/actions/prepare-api-reference with: - path: apps/web/.data/api-reference - key: api-reference-v2-${{ runner.os }}-v3-${{ steps.effect-revisions.outputs.v3 }}-v4-${{ steps.effect-revisions.outputs.v4 }}-${{ hashFiles('packages/api-reference/src/generate.mjs', 'packages/api-reference/package.json', 'packages/domain/src/ApiReference.ts', 'pnpm-lock.yaml') }} + cache: true - - name: Generate API reference - if: steps.api-reference-cache.outputs.cache-hit != 'true' + - name: Deploy Cloudflare preview run: | - vp run api-reference:generate --version v3 --repo .effect-source-v3 - vp run api-reference:generate --version v4 --repo .effect-source-v4 - - - name: Synchronize Mixedbread preview store - if: steps.search-changes.outputs.changed == 'true' - id: mixedbread - run: vp exec mixedbread sync --pr "${{ github.event.pull_request.number }}" --sha "${{ github.event.pull_request.head.sha }}" - env: - MXBAI_API_KEY: ${{ secrets.MXBAI_PREVIEW_ADMIN_API_KEY }} - - - name: Pull Vercel Environment Information - run: pnpm exec vercel pull --cwd apps/web --yes --environment=preview --git-branch="${{ github.head_ref }}" --token=${{ secrets.VERCEL_TOKEN }} --scope=effect-ts + stage=$(vp exec mixedbread preview-stage --pr "$PULL_REQUEST") + vp exec alchemy deploy --stage "$stage" --yes env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - - - name: Build Project Artifacts - run: pnpm exec vercel build --cwd apps/web --token=${{ secrets.VERCEL_TOKEN }} - env: - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + MXBAI_ADMIN_API_KEY: ${{ secrets.MXBAI_PREVIEW_ADMIN_API_KEY }} + MXBAI_API_KEY: ${{ secrets.MXBAI_SEARCH_API_KEY }} + MXBAI_VECTOR_STORE_ID: ${{ secrets.MXBAI_VECTOR_STORE_ID }} + MXBAI_PREVIEW_STORE_ENABLED: ${{ steps.search-changes.outputs.changed }} + KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} + KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} + PUBLIC_POSTHOG_KEY: ${{ vars.PUBLIC_POSTHOG_KEY }} + PUBLIC_POSTHOG_API_HOST: ${{ vars.PUBLIC_POSTHOG_API_HOST }} + PULL_REQUEST: ${{ github.event.pull_request.number }} + GITHUB_SHA: ${{ github.event.pull_request.head.sha }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Verify generated API reference links - run: node apps/web/scripts/verify-api-reference-links.mjs - - - name: Patch trailing-slash redirect into build output - run: node apps/web/scripts/patch-vercel-trailing-slash.mjs - - - name: Deploy Project Artifacts to Vercel run: | - if [[ -z "$SEARCH_STORE_ID" ]]; then - echo "::error::No Mixedbread search store is available for this preview" - exit 1 + if [[ -d apps/web/dist/client/docs ]]; then + node apps/web/scripts/verify-api-reference-links.mjs apps/web/dist/client + else + echo "Alchemy reused the unchanged deployment; no local build output to verify." fi - pnpm exec vercel deploy apps/web --prebuilt --archive=tgz --env="MXBAI_VECTOR_STORE_ID=${SEARCH_STORE_ID}" --token=${{ secrets.VERCEL_TOKEN }} --scope=effect-ts - env: - SEARCH_STORE_ID: ${{ steps.mixedbread.outputs.store_id || secrets.MXBAI_VECTOR_STORE_ID }} - VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} - VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} - cleanup: if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest @@ -140,15 +78,26 @@ jobs: with: ref: ${{ github.event.pull_request.base.sha }} - - name: Install Vite+ - uses: voidzero-dev/setup-vp@v1.17.0 - with: - node-manager: false - - name: Install dependencies uses: ./.github/actions/setup + - name: Destroy Cloudflare preview + run: | + stage=$(vp exec mixedbread preview-stage --pr "$PR_NUMBER") + vp exec alchemy destroy --stage "$stage" --yes + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + MXBAI_ADMIN_API_KEY: ${{ secrets.MXBAI_PREVIEW_ADMIN_API_KEY }} + MXBAI_API_KEY: ${{ secrets.MXBAI_SEARCH_API_KEY }} + MXBAI_VECTOR_STORE_ID: ${{ secrets.MXBAI_VECTOR_STORE_ID }} + KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }} + KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Delete Mixedbread preview store + if: always() run: vp exec mixedbread delete-preview --pr "${{ github.event.pull_request.number }}" env: - MXBAI_API_KEY: ${{ secrets.MXBAI_PREVIEW_ADMIN_API_KEY }} + MXBAI_ADMIN_API_KEY: ${{ secrets.MXBAI_PREVIEW_ADMIN_API_KEY }} diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 9da6a42d4..7e6ef7581 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -1,4 +1,4 @@ -name: Vercel Production Deployment +name: Production Deployment on: workflow_dispatch: @@ -23,23 +23,12 @@ on: permissions: contents: read -concurrency: - group: production - cancel-in-progress: false - jobs: deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: ./.github/actions/deploy - with: - website-ref: ${{ github.sha }} - api-reference-snapshot: ${{ inputs.api_reference_snapshot || '' }} - mixedbread-scope: ${{ inputs.mixedbread_scope || 'markdown' }} - mxbai-api-key: ${{ secrets.MXBAI_API_KEY }} - mxbai-vector-store-id: ${{ secrets.MXBAI_VECTOR_STORE_ID }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-token: ${{ secrets.VERCEL_TOKEN }} + uses: ./.github/workflows/deploy.yml + with: + website-ref: ${{ github.sha }} + api-reference-snapshot: ${{ inputs.api_reference_snapshot || '' }} + mixedbread-scope: ${{ inputs.mixedbread_scope || 'markdown' }} + cloudflare-traffic-mode: ${{ vars.CLOUDFLARE_TRAFFIC_MODE || 'workers-dev' }} + secrets: inherit diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 730fb6fb4..3ef453055 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,12 +22,15 @@ concurrency: cancel-in-progress: false jobs: - publish: + resolve-snapshot: runs-on: ubuntu-latest - permissions: - contents: write outputs: snapshot_tag: ${{ steps.snapshot.outputs.tag }} + snapshot_id: ${{ steps.snapshot.outputs.id }} + generator: ${{ steps.snapshot.outputs.generator }} + published: ${{ steps.snapshot.outputs.published }} + v3_revision: ${{ steps.snapshot.outputs.v3 }} + v4_revision: ${{ steps.snapshot.outputs.v4 }} steps: - uses: actions/checkout@v6 @@ -44,159 +47,72 @@ jobs: V3_REVISION: ${{ inputs.v3_revision }} V4_REVISION: ${{ inputs.v4_revision }} run: | - if [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]]; then - if [[ "$EVENT_REPOSITORY" != "Effect-TS/effect" ]]; then - echo "::error::API reference dispatch must identify Effect-TS/effect as its source repository" - exit 1 - fi - if [[ ! "$EVENT_CHANNEL" =~ ^v[34]$ || ! "$EVENT_REVISION" =~ ^[a-f0-9]{40}$ ]]; then - echo "::error::API reference dispatch must provide channel v3 or v4 and a full Git commit SHA" - exit 1 - fi - - previous_tag=$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --jq '[.[] | select(.draft == false and .prerelease == false and (.tag_name | startswith("api-reference-")))] | sort_by(.published_at) | last | (.tag_name // "")') - if [[ ! "$previous_tag" =~ ^api-reference-[a-f0-9]{64}$ ]]; then - echo "::error::No published API reference snapshot is available. Run this workflow manually with both released revisions to create the initial snapshot." - exit 1 - fi - - previous_dir="$RUNNER_TEMP/previous-api-reference" - mkdir -p "$previous_dir" - gh release download "$previous_tag" --dir "$previous_dir" --pattern manifest.json - V3_REVISION=$(jq -r '.channels.v3.revision // ""' "$previous_dir/manifest.json") - V4_REVISION=$(jq -r '.channels.v4.revision // ""' "$previous_dir/manifest.json") - if [[ "$EVENT_CHANNEL" == "v3" ]]; then - V3_REVISION="$EVENT_REVISION" - else - V4_REVISION="$EVENT_REVISION" - fi - fi - - if [[ ! "$V3_REVISION" =~ ^[a-f0-9]{40}$ || ! "$V4_REVISION" =~ ^[a-f0-9]{40}$ ]]; then - echo "::error::API reference revisions must be full Git commit SHAs" - exit 1 - fi - - generator_hash=$(git hash-object \ - packages/api-reference/src/generate.mjs \ - packages/api-reference/src/snapshot.mjs \ - packages/api-reference/package.json \ - packages/domain/src/ApiReference.ts \ - packages/domain/src/ApiReferenceSnapshot.ts \ - pnpm-lock.yaml | git hash-object --stdin) - snapshot_id=$(vp exec api-reference snapshot id \ + vp exec api-reference snapshot prepare \ + --event-name "$GITHUB_EVENT_NAME" \ + --event-repository "$EVENT_REPOSITORY" \ + --event-channel "$EVENT_CHANNEL" \ + --event-revision "$EVENT_REVISION" \ + --repository "$GITHUB_REPOSITORY" \ --v3 "$V3_REVISION" \ - --v4 "$V4_REVISION" \ - --generator "$generator_hash") - tag="api-reference-$snapshot_id" - - published=false - if release=$(gh release view "$tag" --json isDraft 2>/dev/null); then - if [[ "$(jq -r .isDraft <<<"$release")" == "true" ]]; then - gh release delete "$tag" --yes --cleanup-tag - else - published=true - fi - fi + --v4 "$V4_REVISION" >> "$GITHUB_OUTPUT" - echo "v3=$V3_REVISION" >> "$GITHUB_OUTPUT" - echo "v4=$V4_REVISION" >> "$GITHUB_OUTPUT" - echo "generator=$generator_hash" >> "$GITHUB_OUTPUT" - echo "id=$snapshot_id" >> "$GITHUB_OUTPUT" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "published=$published" >> "$GITHUB_OUTPUT" + publish-snapshot: + needs: resolve-snapshot + if: needs.resolve-snapshot.outputs.published != 'true' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 - - name: Checkout Effect v3 - if: steps.snapshot.outputs.published != 'true' - uses: actions/checkout@v6 - with: - repository: Effect-TS/effect - ref: ${{ steps.snapshot.outputs.v3 }} - path: .effect-source-v3 - fetch-depth: 1 + - name: Install dependencies + uses: ./.github/actions/setup - - name: Checkout Effect v4 - if: steps.snapshot.outputs.published != 'true' - uses: actions/checkout@v6 + - name: Prepare API reference + uses: ./.github/actions/prepare-api-reference with: - repository: Effect-TS/effect - ref: ${{ steps.snapshot.outputs.v4 }} - path: .effect-source-v4 - fetch-depth: 1 - - - name: Generate API reference - if: steps.snapshot.outputs.published != 'true' - run: | - vp run api-reference:generate --version v3 --repo .effect-source-v3 - vp run api-reference:generate --version v4 --repo .effect-source-v4 + v3-revision: ${{ needs.resolve-snapshot.outputs.v3_revision }} + v4-revision: ${{ needs.resolve-snapshot.outputs.v4_revision }} - name: Package API reference snapshot - if: steps.snapshot.outputs.published != 'true' env: - GENERATOR_HASH: ${{ steps.snapshot.outputs.generator }} - SNAPSHOT_ID: ${{ steps.snapshot.outputs.id }} - V3_REVISION: ${{ steps.snapshot.outputs.v3 }} - V4_REVISION: ${{ steps.snapshot.outputs.v4 }} + GENERATOR_HASH: ${{ needs.resolve-snapshot.outputs.generator }} + SNAPSHOT_ID: ${{ needs.resolve-snapshot.outputs.snapshot_id }} + V3_REVISION: ${{ needs.resolve-snapshot.outputs.v3_revision }} + V4_REVISION: ${{ needs.resolve-snapshot.outputs.v4_revision }} run: | - asset_dir="$RUNNER_TEMP/api-reference-assets" - mkdir -p "$asset_dir" - vp exec api-reference snapshot create \ + vp exec api-reference snapshot package \ --data apps/web/.data/api-reference \ - --output "$asset_dir/manifest.json" \ + --output "$RUNNER_TEMP/api-reference-assets" \ --v3 "$V3_REVISION" \ --v4 "$V4_REVISION" \ --generator "$GENERATOR_HASH" \ - --website-revision "$GITHUB_SHA" - tar -czf "$asset_dir/api-reference.tar.gz" -C apps/web/.data api-reference - sha256sum "$asset_dir/api-reference.tar.gz" | sed "s|$asset_dir/||" > "$asset_dir/api-reference.sha256" - - manifest_id=$(vp exec api-reference snapshot validate \ - --data apps/web/.data/api-reference \ - --manifest "$asset_dir/manifest.json") - if [[ "$manifest_id" != "$SNAPSHOT_ID" ]]; then - echo "::error::Generated API reference snapshot ID changed during packaging" - exit 1 - fi + --website-revision "$GITHUB_SHA" \ + --expected-id "$SNAPSHOT_ID" - name: Publish API reference snapshot - if: steps.snapshot.outputs.published != 'true' env: GH_TOKEN: ${{ github.token }} - SNAPSHOT_TAG: ${{ steps.snapshot.outputs.tag }} - V3_REVISION: ${{ steps.snapshot.outputs.v3 }} - V4_REVISION: ${{ steps.snapshot.outputs.v4 }} + V3_REVISION: ${{ needs.resolve-snapshot.outputs.v3_revision }} + V4_REVISION: ${{ needs.resolve-snapshot.outputs.v4_revision }} + GENERATOR_HASH: ${{ needs.resolve-snapshot.outputs.generator }} + SNAPSHOT_ID: ${{ needs.resolve-snapshot.outputs.snapshot_id }} run: | - asset_dir="$RUNNER_TEMP/api-reference-assets" - gh release create "$SNAPSHOT_TAG" \ - --draft \ - --latest=false \ - --title "API reference snapshot ${SNAPSHOT_TAG#api-reference-}" \ - --notes "Effect v3: $V3_REVISION - Effect v4: $V4_REVISION" - gh release upload "$SNAPSHOT_TAG" \ - "$asset_dir/api-reference.tar.gz" \ - "$asset_dir/api-reference.sha256" \ - "$asset_dir/manifest.json" - gh release edit "$SNAPSHOT_TAG" --draft=false - - deploy: - needs: publish - runs-on: ubuntu-latest - concurrency: - group: production - cancel-in-progress: false - permissions: - contents: read - steps: - - uses: actions/checkout@v6 - - - uses: ./.github/actions/deploy - with: - website-ref: ${{ github.sha }} - api-reference-snapshot: ${{ needs.publish.outputs.snapshot_tag }} - mixedbread-scope: api-reference - mxbai-api-key: ${{ secrets.MXBAI_API_KEY }} - mxbai-vector-store-id: ${{ secrets.MXBAI_VECTOR_STORE_ID }} - vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} - vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-token: ${{ secrets.VERCEL_TOKEN }} + vp exec api-reference snapshot publish \ + --repository "$GITHUB_REPOSITORY" \ + --assets "$RUNNER_TEMP/api-reference-assets" \ + --v3 "$V3_REVISION" \ + --v4 "$V4_REVISION" \ + --generator "$GENERATOR_HASH" \ + --expected-id "$SNAPSHOT_ID" + + deploy-production: + needs: [resolve-snapshot, publish-snapshot] + if: always() && needs.resolve-snapshot.result == 'success' && (needs.publish-snapshot.result == 'success' || needs.publish-snapshot.result == 'skipped') + uses: ./.github/workflows/deploy.yml + with: + website-ref: ${{ github.sha }} + api-reference-snapshot: ${{ needs.resolve-snapshot.outputs.snapshot_tag }} + mixedbread-scope: api-reference + cloudflare-traffic-mode: ${{ vars.CLOUDFLARE_TRAFFIC_MODE || 'workers-dev' }} + secrets: inherit diff --git a/.gitignore b/.gitignore index 4d9a14b6d..137d5cb93 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,13 @@ pnpm-debug.log* # TypeScript *.tsbuildinfo +# Playwright +playwright-report/ +test-results/ + +# Alchemy +.alchemy + # Vercel .vercel .env* diff --git a/AGENTS.md b/AGENTS.md index 9f2f5a474..970d0067c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,13 @@ Docs are local at `node_modules/vite-plus/docs` or online at https://viteplus.de `vp ` runs a built-in command. `vp run ` runs a `package.json` script or a `vite.config.ts` task. Scripts cannot overwrite built-ins, so `vp dev` and `vp run dev` may do different things. Check `package.json` and `vite.config.ts` first, and run `vp run ` when the project defines a script or task with that name. +## Tool Versions + +Run `vp toolchain` to show versions and relationships in the active Vite+ +release. Add a tool name to select part of the graph. For example, run +`vp toolchain vite`. Use `--global` to ignore the local `vite-plus` package. Use +`vp why ` to show the package-manager dependency graph. + ## Review Checklist - [ ] Run `vp install` after pulling remote changes and before getting started. diff --git a/alchemy.run.ts b/alchemy.run.ts new file mode 100644 index 000000000..613e35e93 --- /dev/null +++ b/alchemy.run.ts @@ -0,0 +1,181 @@ +import * as Alchemy from "alchemy" +import * as Cloudflare from "alchemy/Cloudflare" +import * as Command from "alchemy/Command" +import * as GitHub from "alchemy/GitHub" +import type { Input } from "alchemy/Input" +import * as Output from "alchemy/Output" +import * as Mixedbread from "@website/alchemy-mixedbread" +import * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Redacted from "effect/Redacted" +import * as Schema from "effect/Schema" + +const TrafficMode = Schema.Literals([ + "workers-dev", + "routes", + "bridge", + "custom-domains", +]) + +const Website = (storeId: Input>) => + Cloudflare.Website.Astro( + "WebsiteWorker", + Effect.gen(function* () { + const stage = yield* Alchemy.Stage + const trafficMode = + stage === "prod" + ? yield* Config.schema(TrafficMode, "CLOUDFLARE_TRAFFIC_MODE").pipe( + Config.withDefault("workers-dev"), + Effect.orDie, + ) + : "workers-dev" + const domainEnabled = + trafficMode === "bridge" || trafficMode === "custom-domains" + const routeEnabled = trafficMode === "routes" || trafficMode === "bridge" + + return { + rootDir: "./apps/web", + memo: { + include: [ + ".data/api-reference/**", + "src/**", + "public/**", + "astro.config.ts", + "package.json", + "tsconfig.json", + ], + exclude: ["dist/**", ".astro/**", "node_modules/**"], + lockfile: true, + workspaces: "auto" as const, + }, + astro: { output: "static" as const }, + assets: { htmlHandling: "drop-trailing-slash" as const }, + ...(stage === "prod" + ? { + domain: domainEnabled + ? { + name: "effect.website", + redirects: ["www.effect.website"], + } + : null, + routes: routeEnabled + ? [ + { + pattern: "effect.website/*", + zoneName: "effect.website", + }, + ] + : [], + } + : {}), + workersDev: true, + cache: { + enabled: true, + crossVersionCache: true, + }, + env: { + MXBAI_API_KEY: Config.redacted("MXBAI_API_KEY"), + MXBAI_VECTOR_STORE_ID: storeId, + KV_REST_API_URL: Config.redacted("KV_REST_API_URL"), + KV_REST_API_TOKEN: Config.redacted("KV_REST_API_TOKEN"), + }, + sessionKVBindingName: false, + prerenderEnvironment: "node" as const, + } + }), + ) + +export default Alchemy.Stack( + "EffectWebsite", + { + providers: Layer.mergeAll( + Cloudflare.providers(), + GitHub.providers(), + Mixedbread.providers(), + ), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const pullRequest = yield* Config.option(Config.int("PULL_REQUEST")) + const previewStoreEnabled = yield* Config.boolean( + "MXBAI_PREVIEW_STORE_ENABLED", + ).pipe(Config.withDefault(false)) + const sha = yield* Config.string("GITHUB_SHA") + let storeId: Input> = Config.redacted( + "MXBAI_VECTOR_STORE_ID", + ) + + if (previewStoreEnabled && Option.isSome(pullRequest)) { + const store = yield* Mixedbread.VectorStore("PreviewSearchStore", { + name: `effect-website-pr-${pullRequest.value}`, + description: `Effect website search preview for PR ${pullRequest.value}`, + expiresAfter: { anchor: "last_active_at", days: 7 }, + metadata: { + lifecycle: "pull-request-preview", + pullRequest: pullRequest.value, + repository: "Effect-TS/website", + }, + config: { + contextualization: { + with_file_context: true, + with_metadata: ["file_path"], + }, + }, + }) + const synchronization = yield* Command.Exec("SynchronizePreviewSearch", { + command: `vp exec mixedbread sync --pr ${pullRequest.value} --sha ${sha}`, + env: { + MXBAI_VECTOR_STORE_ID: store.id, + }, + memo: { + include: [ + "apps/web/src/content/docs/**", + "apps/web/src/content/blog/**", + "apps/web/src/content.config.ts", + "apps/web/.data/api-reference/**", + "apps/web/src/features/api-reference/**", + "apps/web/src/features/search/domain.ts", + "packages/api-reference/**", + "packages/domain/**", + "packages/mixedbread/**", + "pnpm-lock.yaml", + ], + }, + timeout: "30 minutes", + }) + storeId = Output.all(store.id, synchronization.hash.input).pipe( + Output.map(([id]) => id), + ) + } + + const website = yield* Website(storeId) + + yield* Option.match(pullRequest, { + onNone: () => Effect.void, + onSome: (issueNumber) => + Effect.gen(function* () { + yield* GitHub.Comment("preview-comment", { + owner: "Effect-TS", + repository: "website", + issueNumber, + body: Output.interpolate` + ## Cloudflare Preview + + **URL:** ${website.url} + + Built from commit [${sha.slice(0, 7)}](https://github.com/Effect-TS/website/commit/${sha}). + + --- + _This comment updates automatically with each deployment._ + `, + }) + }), + }) + + return { + websiteUrl: website.url, + } + }), +) diff --git a/apps/web/astro.config.ts b/apps/web/astro.config.ts index 732c55581..b8a8b920e 100644 --- a/apps/web/astro.config.ts +++ b/apps/web/astro.config.ts @@ -1,9 +1,8 @@ import mdx from "@astrojs/mdx" import react from "@astrojs/react" -import vercel from "@astrojs/vercel" +// import vercel from "@astrojs/vercel" import tailwindcss from "@tailwindcss/vite" import expressiveCode from "astro-expressive-code" -import icon from "astro-icon" import { defineConfig, envField, @@ -12,6 +11,7 @@ import { } from "astro/config" import { fileURLToPath } from "node:url" import svgr from "vite-plugin-svgr" +import { openGraphMetadataPlugin } from "./src/features/open-graph/plugin" import { monacoEditorPlugin } from "./src/features/playground/plugins/monaco-editor" import { docsLegacyRedirectList } from "./src/generated/docs-legacy-redirects" import { twieRedirectList } from "./src/generated/twie-redirects" @@ -22,7 +22,7 @@ const FontsourceProvider = fontProviders.fontsource() const config = defineConfig({ site: "https://effect.website", - adapter: vercel(), + // adapter: vercel(), trailingSlash: "never", @@ -37,9 +37,13 @@ const config = defineConfig({ }, vite: { + optimizeDeps: { + entries: ["src/features/playground/index.tsx"], + }, plugins: [ tailwindcss(), svgr(), + openGraphMetadataPlugin(), monacoEditorPlugin({ languages: ["typescript", "javascript", "json", "css", "html"], features: "all", @@ -47,6 +51,7 @@ const config = defineConfig({ ], envDir: fileURLToPath(new URL("../../", import.meta.url)), resolve: { + dedupe: ["react", "react-dom"], alias: { "@/": fileURLToPath(new URL("./src/", import.meta.url)), "@astrojs/starlight/components": fileURLToPath( @@ -84,7 +89,7 @@ const config = defineConfig({ }, }, - integrations: [expressiveCode(), react(), mdx(), icon()], + integrations: [expressiveCode(), react(), mdx()], fonts: [ { @@ -160,18 +165,6 @@ const config = defineConfig({ status: 307, destination: "/docs/v4/api", }, - "/docs/api/[version]": { - status: 308, - destination: "/docs/[version]/api", - }, - "/docs/api/[version]/[package]": { - status: 308, - destination: "/docs/[version]/api/[package]", - }, - "/docs/api/[version]/[package]/[...module]": { - status: 308, - destination: "/docs/[version]/api/[package]/[...module]", - }, }, }) diff --git a/apps/web/package.json b/apps/web/package.json index ec575402b..745340269 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,14 +10,12 @@ "@base-ui/react": "^1.6.0", "@dprint/formatter": "^0.5.1", "@effect/atom-react": "catalog:effect", - "@effect/monaco-editor": "^0.0.2", + "@effect/monaco-editor": "catalog:effect", "@effect/platform-browser": "catalog:effect", "@effect/platform-node": "catalog:effect", "@expressive-code/core": "^0.43.1", - "@iconify-json/fa7-brands": "^1.2.4", "@lucide/astro": "^1.21.0", "@mixedbread/sdk": "^0.77.0", - "@resvg/resvg-js": "^2.6.2", "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/langs": "4.2.0", @@ -25,14 +23,14 @@ "@tanstack/react-table": "^8.21.3", "@upstash/redis": "^1.38.0", "@webcontainer/api": "^1.6.4", - "@website/api-reference": "workspace:*", - "@website/domain": "workspace:*", + "@website/api-reference": "workspace:^", + "@website/domain": "workspace:^", + "@website/open-graph": "workspace:^", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "@zip.js/zip.js": "^2.8.26", - "astro": "^7.0.3", + "astro": "^7.2.2", "astro-expressive-code": "^0.43.1", - "astro-icon": "^1.1.5", "astro-seo": "^1.1.0", "astro-tweet": "^0.0.6", "class-variance-authority": "^0.7.1", @@ -44,7 +42,6 @@ "react": "^19.2.7", "react-dom": "^19.2.7", "react-resizable-panels": "^4.12.1", - "satori": "^0.26.0", "sharp": "^0.35.1", "shiki": "^4.2.0", "srt-parser-2": "^1.2.3", @@ -62,6 +59,7 @@ "@tailwindcss/vite": "^4.3.1", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "devalue": "^5.9.0", "shadcn": "^4.11.0", "vite": "catalog:", "vite-plugin-svgr": "^5.2.0" diff --git a/apps/web/public/_headers b/apps/web/public/_headers new file mode 100644 index 000000000..983a5344c --- /dev/null +++ b/apps/web/public/_headers @@ -0,0 +1,19 @@ +/_astro/editor.worker-* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin + +/_astro/ts.worker-* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin + +/_astro/json.worker-* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin + +/_astro/css.worker-* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin + +/_astro/html.worker-* + Cross-Origin-Embedder-Policy: require-corp + Cross-Origin-Opener-Policy: same-origin diff --git a/apps/web/src/pages/og/_assets/adoption-partners/ziverge.png b/apps/web/public/og/adoption-partners/ziverge.png similarity index 100% rename from apps/web/src/pages/og/_assets/adoption-partners/ziverge.png rename to apps/web/public/og/adoption-partners/ziverge.png diff --git a/apps/web/src/pages/og/_assets/base.png b/apps/web/public/og/base.png similarity index 100% rename from apps/web/src/pages/og/_assets/base.png rename to apps/web/public/og/base.png diff --git a/apps/web/src/pages/og/_assets/blog/base.png b/apps/web/public/og/blog/base.png similarity index 100% rename from apps/web/src/pages/og/_assets/blog/base.png rename to apps/web/public/og/blog/base.png diff --git a/apps/web/src/pages/og/_assets/brand-assets.png b/apps/web/public/og/brand-assets.png similarity index 100% rename from apps/web/src/pages/og/_assets/brand-assets.png rename to apps/web/public/og/brand-assets.png diff --git a/apps/web/src/pages/og/_assets/community-hub.png b/apps/web/public/og/community-hub.png similarity index 100% rename from apps/web/src/pages/og/_assets/community-hub.png rename to apps/web/public/og/community-hub.png diff --git a/apps/web/src/pages/og/_assets/docs/base.png b/apps/web/public/og/docs/base.png similarity index 100% rename from apps/web/src/pages/og/_assets/docs/base.png rename to apps/web/public/og/docs/base.png diff --git a/apps/web/src/pages/og/_assets/effect-days.png b/apps/web/public/og/effect-days.png similarity index 100% rename from apps/web/src/pages/og/_assets/effect-days.png rename to apps/web/public/og/effect-days.png diff --git a/apps/web/src/pages/og/_assets/effect-days/about-livorno.png b/apps/web/public/og/effect-days/about-livorno.png similarity index 100% rename from apps/web/src/pages/og/_assets/effect-days/about-livorno.png rename to apps/web/public/og/effect-days/about-livorno.png diff --git a/apps/web/src/pages/og/_assets/effect-days/code-of-conduct.png b/apps/web/public/og/effect-days/code-of-conduct.png similarity index 100% rename from apps/web/src/pages/og/_assets/effect-days/code-of-conduct.png rename to apps/web/public/og/effect-days/code-of-conduct.png diff --git a/apps/web/src/pages/og/_assets/effect-days/refund-policy.png b/apps/web/public/og/effect-days/refund-policy.png similarity index 100% rename from apps/web/src/pages/og/_assets/effect-days/refund-policy.png rename to apps/web/public/og/effect-days/refund-policy.png diff --git a/apps/web/src/pages/og/_assets/effect-jobs.png b/apps/web/public/og/effect-jobs.png similarity index 100% rename from apps/web/src/pages/og/_assets/effect-jobs.png rename to apps/web/public/og/effect-jobs.png diff --git a/apps/web/src/pages/og/_assets/index.png b/apps/web/public/og/index.png similarity index 100% rename from apps/web/src/pages/og/_assets/index.png rename to apps/web/public/og/index.png diff --git a/apps/web/src/pages/og/_assets/merch.png b/apps/web/public/og/merch.png similarity index 100% rename from apps/web/src/pages/og/_assets/merch.png rename to apps/web/public/og/merch.png diff --git a/apps/web/src/pages/og/_assets/myths.png b/apps/web/public/og/myths.png similarity index 100% rename from apps/web/src/pages/og/_assets/myths.png rename to apps/web/public/og/myths.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/adopting-effect-at-zendesk-with-attila-vecerek.png b/apps/web/public/og/podcast/episodes/adopting-effect-at-zendesk-with-attila-vecerek.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/adopting-effect-at-zendesk-with-attila-vecerek.png rename to apps/web/public/og/podcast/episodes/adopting-effect-at-zendesk-with-attila-vecerek.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/effectifying-opencode-with-kit-langton.png b/apps/web/public/og/podcast/episodes/effectifying-opencode-with-kit-langton.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/effectifying-opencode-with-kit-langton.png rename to apps/web/public/og/podcast/episodes/effectifying-opencode-with-kit-langton.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/event-driven-systems-in-fintech-how-spiko-leverages-effect-with-samuel-briole.png b/apps/web/public/og/podcast/episodes/event-driven-systems-in-fintech-how-spiko-leverages-effect-with-samuel-briole.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/event-driven-systems-in-fintech-how-spiko-leverages-effect-with-samuel-briole.png rename to apps/web/public/og/podcast/episodes/event-driven-systems-in-fintech-how-spiko-leverages-effect-with-samuel-briole.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/foldkit-an-effect-first-frontend-framework-with-devin-jameson.png b/apps/web/public/og/podcast/episodes/foldkit-an-effect-first-frontend-framework-with-devin-jameson.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/foldkit-an-effect-first-frontend-framework-with-devin-jameson.png rename to apps/web/public/og/podcast/episodes/foldkit-an-effect-first-frontend-framework-with-devin-jameson.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/from-skeptic-to-advocate-scaling-effect-at-vercel-with-dillon-mulroy.png b/apps/web/public/og/podcast/episodes/from-skeptic-to-advocate-scaling-effect-at-vercel-with-dillon-mulroy.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/from-skeptic-to-advocate-scaling-effect-at-vercel-with-dillon-mulroy.png rename to apps/web/public/og/podcast/episodes/from-skeptic-to-advocate-scaling-effect-at-vercel-with-dillon-mulroy.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/inside-openrouters-tech-stack-and-use-of-effect-with-louis-vichy.png b/apps/web/public/og/podcast/episodes/inside-openrouters-tech-stack-and-use-of-effect-with-louis-vichy.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/inside-openrouters-tech-stack-and-use-of-effect-with-louis-vichy.png rename to apps/web/public/og/podcast/episodes/inside-openrouters-tech-stack-and-use-of-effect-with-louis-vichy.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/reliable-payroll-systems-in-typescript-with-effect-with-adam-rankin.png b/apps/web/public/og/podcast/episodes/reliable-payroll-systems-in-typescript-with-effect-with-adam-rankin.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/reliable-payroll-systems-in-typescript-with-effect-with-adam-rankin.png rename to apps/web/public/og/podcast/episodes/reliable-payroll-systems-in-typescript-with-effect-with-adam-rankin.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/scaling-ai-for-customer-support-at-markprompt-with-effect-with-michael-fester.png b/apps/web/public/og/podcast/episodes/scaling-ai-for-customer-support-at-markprompt-with-effect-with-michael-fester.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/scaling-ai-for-customer-support-at-markprompt-with-effect-with-michael-fester.png rename to apps/web/public/og/podcast/episodes/scaling-ai-for-customer-support-at-markprompt-with-effect-with-michael-fester.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/scaling-voice-ai-at-masterclass-with-effect-and-typescript-with-david-golightly.png b/apps/web/public/og/podcast/episodes/scaling-voice-ai-at-masterclass-with-effect-and-typescript-with-david-golightly.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/scaling-voice-ai-at-masterclass-with-effect-and-typescript-with-david-golightly.png rename to apps/web/public/og/podcast/episodes/scaling-voice-ai-at-masterclass-with-effect-and-typescript-with-david-golightly.png diff --git a/apps/web/src/pages/og/_assets/podcast/episodes/software-engineering-in-the-age-of-ai-with-john-a-de-goes.png b/apps/web/public/og/podcast/episodes/software-engineering-in-the-age-of-ai-with-john-a-de-goes.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/episodes/software-engineering-in-the-age-of-ai-with-john-a-de-goes.png rename to apps/web/public/og/podcast/episodes/software-engineering-in-the-age-of-ai-with-john-a-de-goes.png diff --git a/apps/web/src/pages/og/_assets/podcast/index.png b/apps/web/public/og/podcast/index.png similarity index 100% rename from apps/web/src/pages/og/_assets/podcast/index.png rename to apps/web/public/og/podcast/index.png diff --git a/apps/web/src/pages/og/_assets/privacy.png b/apps/web/public/og/privacy.png similarity index 100% rename from apps/web/src/pages/og/_assets/privacy.png rename to apps/web/public/og/privacy.png diff --git a/apps/web/src/assets/icons/fa7-brands/bluesky.svg b/apps/web/src/assets/icons/fa7-brands/bluesky.svg new file mode 100644 index 000000000..f539d5aac --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/bluesky.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/discord.svg b/apps/web/src/assets/icons/fa7-brands/discord.svg new file mode 100644 index 000000000..c4a343e15 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/discord.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/github.svg b/apps/web/src/assets/icons/fa7-brands/github.svg new file mode 100644 index 000000000..e73cc0c94 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/github.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/linkedin.svg b/apps/web/src/assets/icons/fa7-brands/linkedin.svg new file mode 100644 index 000000000..a8ecc2b59 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/linkedin.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/npm.svg b/apps/web/src/assets/icons/fa7-brands/npm.svg new file mode 100644 index 000000000..aec148c90 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/npm.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/spotify.svg b/apps/web/src/assets/icons/fa7-brands/spotify.svg new file mode 100644 index 000000000..6f0520bdd --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/spotify.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/x-twitter.svg b/apps/web/src/assets/icons/fa7-brands/x-twitter.svg new file mode 100644 index 000000000..3577fc593 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/x-twitter.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/icons/fa7-brands/youtube.svg b/apps/web/src/assets/icons/fa7-brands/youtube.svg new file mode 100644 index 000000000..57954d3b2 --- /dev/null +++ b/apps/web/src/assets/icons/fa7-brands/youtube.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/og/blog/base.png b/apps/web/src/assets/og/blog/base.png new file mode 100644 index 000000000..057cfb09b Binary files /dev/null and b/apps/web/src/assets/og/blog/base.png differ diff --git a/apps/web/src/assets/og/docs/base.png b/apps/web/src/assets/og/docs/base.png new file mode 100644 index 000000000..a07e00c03 Binary files /dev/null and b/apps/web/src/assets/og/docs/base.png differ diff --git a/apps/web/src/components/Footer.astro b/apps/web/src/components/Footer.astro index 4bf4aaea3..e708d283a 100644 --- a/apps/web/src/components/Footer.astro +++ b/apps/web/src/components/Footer.astro @@ -1,7 +1,12 @@ --- import { ArrowUpRight } from "@lucide/astro" -import { Icon } from "astro-icon/components" +import BlueSkyLogo from "@/assets/icons/fa7-brands/bluesky.svg" +import DiscordLogo from "@/assets/icons/fa7-brands/discord.svg" +import GitHubLogo from "@/assets/icons/fa7-brands/github.svg" +import LinkedInLogo from "@/assets/icons/fa7-brands/linkedin.svg" +import TwitterLogo from "@/assets/icons/fa7-brands/x-twitter.svg" +import YouTubeLogo from "@/assets/icons/fa7-brands/youtube.svg" import EffectLogoBlack from "@/assets/logos/effect/combination-mark/svg/effect-logo-black.svg" import EffectLogoWhite from "@/assets/logos/effect/combination-mark/svg/effect-logo-white.svg" import { Link } from "@/components/ui/Link" @@ -274,7 +279,7 @@ const year = new Date().getFullYear() className="text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white" >
-
-
-
-
-
-
diff --git a/apps/web/src/components/SocialLink.astro b/apps/web/src/components/SocialLink.astro index 4bbeae712..ab57876ea 100644 --- a/apps/web/src/components/SocialLink.astro +++ b/apps/web/src/components/SocialLink.astro @@ -1,13 +1,13 @@ --- import type { ImageMetadata } from "astro" import type { SvgComponent } from "astro/types" -import BlueSkyLogo from "@/assets/logos/bluesky/BlueSky.svg" -import DiscordLogo from "@/assets/logos/discord/Discord.svg" -import GitHubLogo from "@/assets/logos/github/GitHub.svg" -import LinkedInLogo from "@/assets/logos/linkedin/LinkedIn.svg" -import SpotifyLogo from "@/assets/logos/spotify/Spotify.svg" -import TwitterLogo from "@/assets/logos/twitter/Twitter.svg" -import YouTubeLogo from "@/assets/logos/youtube/YouTube.svg" +import BlueSkyLogo from "@/assets/icons/fa7-brands/bluesky.svg" +import DiscordLogo from "@/assets/icons/fa7-brands/discord.svg" +import GitHubLogo from "@/assets/icons/fa7-brands/github.svg" +import LinkedInLogo from "@/assets/icons/fa7-brands/linkedin.svg" +import SpotifyLogo from "@/assets/icons/fa7-brands/spotify.svg" +import TwitterLogo from "@/assets/icons/fa7-brands/x-twitter.svg" +import YouTubeLogo from "@/assets/icons/fa7-brands/youtube.svg" import { cn } from "@/lib/utils" import Link from "./Link.astro" diff --git a/apps/web/src/components/landing/blocks/HeroCommandPanel.astro b/apps/web/src/components/landing/blocks/HeroCommandPanel.astro index 291a74838..1096cda3d 100644 --- a/apps/web/src/components/landing/blocks/HeroCommandPanel.astro +++ b/apps/web/src/components/landing/blocks/HeroCommandPanel.astro @@ -11,7 +11,7 @@ import { } from "@/lib/install-command" import BunLogo from "@/assets/logos/bun/Bun.svg" import DenoLogo from "@/assets/logos/deno/Deno.svg" -import NpmFullLogo from "@/assets/logos/npm/NpmFull.svg" +import NpmFullLogo from "@/assets/icons/fa7-brands/npm.svg" import PnpmLogo from "@/assets/logos/pnpm/Pnpm.svg" import YarnLogo from "@/assets/logos/yarn/Yarn.svg" diff --git a/apps/web/src/components/landing/sections/CallToAction.astro b/apps/web/src/components/landing/sections/CallToAction.astro index 8d509eb4f..f751162b9 100644 --- a/apps/web/src/components/landing/sections/CallToAction.astro +++ b/apps/web/src/components/landing/sections/CallToAction.astro @@ -1,6 +1,6 @@ --- import { ArrowRight } from "@lucide/astro" -import DiscordLogo from "@/assets/logos/discord/Discord.svg" +import DiscordLogo from "@/assets/icons/fa7-brands/discord.svg" import GridBackground from "@/components/GridBackground.astro" import HeroCommandPanel from "@/components/landing/blocks/HeroCommandPanel.astro" --- diff --git a/apps/web/src/components/landing/sections/Examples.astro b/apps/web/src/components/landing/sections/Examples.astro index 6f7589864..87796d1ac 100644 --- a/apps/web/src/components/landing/sections/Examples.astro +++ b/apps/web/src/components/landing/sections/Examples.astro @@ -4,7 +4,7 @@ import VisualEffects from "@/features/visual-effect/components/VisualEffects"
- +

diff --git a/apps/web/src/components/landing/sections/FAQ.astro b/apps/web/src/components/landing/sections/FAQ.astro index 65d944c8f..02ea6b86d 100644 --- a/apps/web/src/components/landing/sections/FAQ.astro +++ b/apps/web/src/components/landing/sections/FAQ.astro @@ -1,5 +1,5 @@ --- -import DiscordLogo from "@/assets/logos/discord/Discord.svg" +import DiscordLogo from "@/assets/icons/fa7-brands/discord.svg" import FrequentlyAskedQuestions from "../blocks/FrequentlyAskedQuestions.astro" --- diff --git a/apps/web/src/components/landing/sections/Quotes.astro b/apps/web/src/components/landing/sections/Quotes.astro index ad5fec47d..167d0f006 100644 --- a/apps/web/src/components/landing/sections/Quotes.astro +++ b/apps/web/src/components/landing/sections/Quotes.astro @@ -6,7 +6,7 @@ import CloudflareLogo from "@/assets/logos/cloudflare/Cloudflare.svg" import MasterclassLogo from "@/assets/logos/masterclass/Masterclass.svg" import SpikoLogo from "@/assets/logos/spiko/Spiko.svg" import TotalTypeScriptLogo from "@/assets/logos/total-typescript/TotalTypescript.png" -import TwitterLogo from "@/assets/logos/twitter/TwitterWhite.svg" +import TwitterLogo from "@/assets/icons/fa7-brands/x-twitter.svg" import UnionBuildLogo from "@/assets/logos/union-build/UnionBuild.svg" import VercelLogo from "@/assets/logos/vercel/Vercel.svg" diff --git a/apps/web/src/components/navigation/Navigation.astro b/apps/web/src/components/navigation/Navigation.astro index fca8aa279..826f7a799 100644 --- a/apps/web/src/components/navigation/Navigation.astro +++ b/apps/web/src/components/navigation/Navigation.astro @@ -70,4 +70,4 @@ const mobileSocialLinks = getNavigationLinks("mobile", "social")

- + diff --git a/apps/web/src/components/navigation/NavigationDesktopActions.astro b/apps/web/src/components/navigation/NavigationDesktopActions.astro index 405dcb286..bb38824e4 100644 --- a/apps/web/src/components/navigation/NavigationDesktopActions.astro +++ b/apps/web/src/components/navigation/NavigationDesktopActions.astro @@ -10,7 +10,7 @@ interface Props { const { showThemeToggle = false } = Astro.props --- - + diff --git a/apps/web/src/components/navigation/NavigationMobileMenu.astro b/apps/web/src/components/navigation/NavigationMobileMenu.astro index 6e40e13d8..73c044fe1 100644 --- a/apps/web/src/components/navigation/NavigationMobileMenu.astro +++ b/apps/web/src/components/navigation/NavigationMobileMenu.astro @@ -1,9 +1,9 @@ --- import { Search, X } from "@lucide/astro" import type { NavigationActiveSlug, NavigationLink } from "@/lib/navigation" -import DiscordIcon from "@/assets/logos/discord/Discord.svg" -import GitHubIcon from "@/assets/logos/github/GitHub.svg" -import TwitterIcon from "@/assets/logos/twitter/Twitter.svg" +import DiscordIcon from "@/assets/icons/fa7-brands/discord.svg" +import GitHubIcon from "@/assets/icons/fa7-brands/github.svg" +import TwitterIcon from "@/assets/icons/fa7-brands/x-twitter.svg" import ThemeToggle from "@/components/ui/ThemeToggle" interface Props { diff --git a/apps/web/src/components/navigation/SearchDialog.tsx b/apps/web/src/components/navigation/SearchDialog.tsx index 97548343e..17ad85ee3 100644 --- a/apps/web/src/components/navigation/SearchDialog.tsx +++ b/apps/web/src/components/navigation/SearchDialog.tsx @@ -5,11 +5,12 @@ import { useAtomValue, } from "@effect/atom-react" import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore" -import * as Data from "effect/Data" -import * as Effect from "effect/Effect" +import * as Cause from "effect/Cause" import { constVoid } from "effect/Function" import * as Option from "effect/Option" +import * as Result from "effect/Result" import * as Schema from "effect/Schema" +import { HttpClientError } from "effect/unstable/http" import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import * as Atom from "effect/unstable/reactivity/Atom" import { @@ -45,19 +46,21 @@ import { SearchAnalytics, type SearchFailureReason, type SearchOpenSource, -} from "@/services/search/analytics" +} from "@/features/search/analytics" +import { SearchClient } from "@/features/search/client" import { + type SearchError, SearchResult, type ApiReferenceSearchResult, type BlogSearchResult, type DocumentationSearchResult, -} from "@/services/search/domain" +} from "@/features/search/domain" import { addRecentSearch, type RecentSearch, RecentSearches as RecentSearchHistory, normalizeRecentSearch, -} from "@/services/search/preferences" +} from "@/features/search/preferences" import { DOCS_VERSIONS, defaultDocsVersion, @@ -161,83 +164,63 @@ type SearchResultsView = | { readonly _tag: "Grouped" } | { readonly _tag: "Section"; readonly section: SearchResult["kind"] } -class SearchError extends Data.TaggedError("SearchError")<{ - readonly cause: unknown +const searchFailure = ( + cause: Cause.Cause, +): { readonly reason: SearchFailureReason readonly httpStatus?: number -}> {} - -const decodeSearchResults = Schema.decodeUnknownEffect( - Schema.Array(SearchResult), -) +} => { + const error = Option.getOrElse(Cause.findErrorOption(cause), () => + Result.getOrElse(Cause.findDefect(cause), () => undefined), + ) -const searchRequestAtom = Atom.make((get) => { - const query = get(debouncedSearchQueryAtom) - if (query.trim().length === 0) { - return Effect.succeed>([]) + if (Cause.isTimeoutError(error)) return { reason: "timeout" } + if (HttpClientError.isHttpClientError(error)) { + if (error.reason._tag === "StatusCodeError") { + return { reason: "http", httpStatus: error.reason.response.status } + } + if ( + error.reason._tag === "TransportError" && + Cause.isTimeoutError(error.reason.cause) + ) { + return { reason: "timeout" } + } + return { + reason: + error.reason._tag === "TransportError" ? "network" : "invalid_response", + } } + if (Schema.isSchemaError(error)) return { reason: "invalid_response" } - get.set(addRecentSearchAtom, { - query, - version: get(selectedVersionAtom), - }) - - const url = `/api/search?query=${encodeURIComponent(query)}` - - return Effect.gen(function* () { - const startedAt = performance.now() - return yield* Effect.gen(function* () { - const response = yield* Effect.tryPromise({ - try: (signal) => fetch(url, { signal }), - catch: (cause) => new SearchError({ cause, reason: "network" }), - }).pipe( - Effect.timeout("5 seconds"), - Effect.catchTag( - "TimeoutError", - (cause) => new SearchError({ cause, reason: "timeout" }), - ), - ) + return { reason: "http", httpStatus: 500 } +} - if (!response.ok) { - return yield* new SearchError({ - cause: new Error(`Search request failed: ${response.status}`), - reason: "http", - httpStatus: response.status, - }) +const searchRequestAtom = Atom.family((query: string) => { + const requestAtom = SearchClient.query("search", "search", { + query: { query }, + }) + let startedAt: number | undefined + + return Atom.transform(requestAtom, (get) => { + const result = get(requestAtom) + if (result.waiting) { + startedAt ??= performance.now() + } else if (startedAt !== undefined) { + const duration = performance.now() - startedAt + startedAt = undefined + if (AsyncResult.isSuccess(result)) { + SearchAnalytics.requestComplete(query, duration, result.value) + } else if (AsyncResult.isFailure(result)) { + const failure = searchFailure(result.cause) + SearchAnalytics.requestFail( + query, + duration, + failure.reason, + failure.httpStatus, + ) } - - const data = yield* Effect.tryPromise({ - try: () => response.json(), - catch: (cause) => - new SearchError({ cause, reason: "invalid_response" }), - }) - - return yield* decodeSearchResults(data).pipe( - Effect.mapError( - (cause) => new SearchError({ cause, reason: "invalid_response" }), - ), - ) - }).pipe( - Effect.tap((results) => - Effect.sync(() => - SearchAnalytics.requestComplete( - query, - performance.now() - startedAt, - results, - ), - ), - ), - Effect.tapError((error) => - Effect.sync(() => - SearchAnalytics.requestFail( - query, - performance.now() - startedAt, - error.reason, - error.httpStatus, - ), - ), - ), - ) + } + return result }) }) @@ -274,7 +257,12 @@ export const allSearchResultsAtom = Atom.make((get) => { return AsyncResult.initial, SearchError>() } - return get(searchRequestAtom) + get.set(addRecentSearchAtom, { + query, + version: get(selectedVersionAtom), + }) + + return get(searchRequestAtom(query)) }) const versionResultsAtom = Atom.make((get) => { diff --git a/apps/web/src/components/ui/toc/TableOfContents.astro b/apps/web/src/components/ui/toc/TableOfContents.astro index 56329531b..d26607e96 100644 --- a/apps/web/src/components/ui/toc/TableOfContents.astro +++ b/apps/web/src/components/ui/toc/TableOfContents.astro @@ -2,14 +2,8 @@ import Eyebrow from "@/components/ui/Eyebrow.astro" import TocLink from "@/components/ui/toc/TocLink.astro" -/** - * "On this page" table of contents — the presentational container shared by - * Docs, API Reference and Blog. It renders the label, divider and the (flat or - * nested) list from a generic `items` prop, and hosts the single scroll-spy - * `